前言
目的
在工作中經(jīng)常會(huì)碰到需要自定義ScrollView的滑塊位置,雖然ScrollView自身的滑塊提供了如scrollBarSize等屬性進(jìn)行設(shè)置,但是到網(wǎng)上搜來搜去也沒有看見能夠?qū)崿F(xiàn)ScrollView的scrollBar能夠居中顯示的方案,加上學(xué)的kotlin一直沒有用到實(shí)戰(zhàn)想練練手。所以,干脆自己畫一個(gè)吧~
最終效果

ps:上圖中的重新滑動(dòng)不會(huì)中斷動(dòng)畫播放的bug已修復(fù)~
思路
繪制
自定義控件繪制當(dāng)然是從onMeasure->onLayout->onDraw開始著手
onMeasure
onMeasure用于測(cè)量控件的寬高,我們的寬高直接在xml中寫死即可,也不需要進(jìn)行額外的測(cè)量流程,無(wú)需重寫
onLayout
onLayout用于測(cè)量控件的位置,也是直接在xml中即可確定,我們無(wú)需重寫
onDraw
重點(diǎn)就在onDraw這里,我們?cè)诶L制時(shí)需要知道如下數(shù)據(jù)
- 當(dāng)前滑塊頂部的位置top
- 當(dāng)前滑塊的高度
- 滑道的寬度和高度(在xml內(nèi)已經(jīng)固定了)
- 滑道的drawable(在xml內(nèi)固定更好,我們先寫死,后續(xù)在xml可配置)
- 滑塊的drawable(在xml內(nèi)固定更好,我們先寫死,后續(xù)在xml可配置)
知道上面的內(nèi)容后,就可以實(shí)現(xiàn)對(duì)應(yīng)的繪制功能
class MyScrollBar(context: Context?, attrs: AttributeSet?) : View(context, attrs) {
var mVerticalThumbHeight: Int = 100//滑塊高度,先暫時(shí)寫死用來看繪制效果
var mVerticalThumbWidth: Int = 0//滑塊寬度
var mVerticalThumbTop: Int = 0//滑塊當(dāng)前起點(diǎn)位置
var mThumbDrawable: Drawable? = null//滑塊drawable
var mTrackDrawable: Drawable? = null//滑道drawable
init {
mThumbDrawable = ContextCompat.getDrawable(getContext(), R.color.colorAccent)
mTrackDrawable = ContextCompat.getDrawable(getContext(), R.color.colorPrimary)
mVerticalThumbWidth = 10
}
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
if (canvas == null) {
return
}
//滑塊的top
val top = mVerticalThumbTop
//滑塊的bottom
val bottom = mVerticalThumbTop + mVerticalThumbHeight
//先繪制滑道
mTrackDrawable?.setBounds(0, 0, mVerticalThumbWidth, measuredHeight)
mTrackDrawable?.draw(canvas)
//再繪制滑塊
mThumbDrawable?.setBounds(0, top, mVerticalThumbWidth, bottom)
mThumbDrawable?.draw(canvas)
}
}
計(jì)算
何時(shí)去修改滑塊位置
- 首次綁定時(shí)
- ScrollView的內(nèi)部TextView內(nèi)容改變時(shí)(目前只支持了TextView,其它的需要自己判斷內(nèi)容改變)
- ScrollView滾動(dòng)時(shí)
/**
* 與ScrollView綁定
* @param nestedScrollView 綁定的ScrollView,由于默認(rèn)的ScrollView不自帶滑動(dòng)監(jiān)聽,所以此處用的是NestedScrollView
*/
fun attachScrollView(nestedScrollView: NestedScrollView) {
nestedScrollView.setOnScrollChangeListener { _, _, _, _, _ ->
calculate(nestedScrollView)
}
val child = nestedScrollView.getChildAt(0)
//由于一般ScrollView的子View都是TextView,這里直接在TextView的內(nèi)容改變時(shí)重新測(cè)量,無(wú)需再手動(dòng)監(jiān)聽
if (child is TextView) {
child.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(
s: CharSequence?,
start: Int,
count: Int,
after: Int
) {
}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
}
override fun afterTextChanged(s: Editable?) {
calculate(nestedScrollView)
}
})
}
post {
//直接調(diào)用會(huì)導(dǎo)致無(wú)法獲取測(cè)量高度
calculate(nestedScrollView)
}
}
如何計(jì)算滑塊位置
private fun calculate(nestedScrollView: NestedScrollView) {
//ScrollView的高度
val visibleHeight = nestedScrollView.measuredHeight
//ScrollView內(nèi)部的內(nèi)容高度
val contentHeight = nestedScrollView.getChildAt(0)?.height ?: 0
//若不需要滾動(dòng),則直接隱藏
if (contentHeight <= visibleHeight) {
visibility = INVISIBLE
return
} else {
visibility = VISIBLE
}
//當(dāng)前ScrollView內(nèi)容滾動(dòng)的距離
val scrollY = nestedScrollView.scrollY
//計(jì)算出滑塊的高度
mVerticalThumbHeight = measuredHeight * visibleHeight / contentHeight
//注意滑塊的top值范圍是從0到{滑道高度-滑塊高度}
mVerticalThumbTop =
(measuredHeight - mVerticalThumbHeight) * scrollY / (contentHeight - visibleHeight)
showNow()
invalidate()
}
實(shí)現(xiàn)隱藏動(dòng)畫
實(shí)現(xiàn)動(dòng)畫之前明確幾點(diǎn)
- 實(shí)現(xiàn)隱藏直接通過設(shè)置ScrollBar的alpha實(shí)現(xiàn)
- 動(dòng)畫通過ObjectAnimator去修改alpha值
- 在滑動(dòng)時(shí)直接設(shè)置alpha為完全可見,并且若正在消失則取消當(dāng)前動(dòng)畫
- NestedScrollView沒有滑動(dòng)狀態(tài)改變的回調(diào),但是可以延遲發(fā)送消失動(dòng)畫執(zhí)行的Runnable,若期間有新滑動(dòng)則取消該Runnable并重新延遲發(fā)送
private val dismissRunnable = Runnable {
if (isShown) {
animator = ObjectAnimator.ofFloat(this, "alpha", alpha, 0f).setDuration(500)
animator?.start()
}
}
/**
* 立刻顯示并延遲消失
*/
private fun showNow() {
animator?.let {
it.end()
it.cancel()
}
alpha = 1f
postDelayDismissRunnable()
}
private fun postDelayDismissRunnable() {
removeCallbacks(dismissRunnable)
postDelayed(dismissRunnable, 1000)
}
使用方式
<?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"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<androidx.core.widget.NestedScrollView
android:id="@+id/scroll_view"
android:layout_width="match_parent"
android:layout_height="500dp"
android:layout_marginStart="24dp"
android:layout_marginTop="24dp"
android:layout_marginEnd="24dp"
android:layout_marginBottom="24dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" >
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/text"/>
</androidx.core.widget.NestedScrollView>
<com.kyrie.demo.scrollbar.MyScrollBar
android:id="@+id/scroll_bar"
android:layout_width="5dp"
android:layout_height="400dp"
android:layout_marginTop="24dp"
android:layout_marginEnd="24dp"
android:layout_marginBottom="24dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
在MainActivity直接調(diào)用ScrollBar的attachScrollView方法即可
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
scroll_bar.attachScrollView(scroll_view)
}
}
總結(jié)
自定義一個(gè)ScrollBar相對(duì)于平常的其它控件來說還是很輕松的,不會(huì)涉及到太多繪制即測(cè)量等方面的知識(shí)。因?yàn)闀r(shí)間比較趕,目前還缺乏以下幾個(gè)功能:
- 實(shí)現(xiàn)橫向滑動(dòng)
- 通過xml配置透明度、延遲消失時(shí)間等
- 通過拖動(dòng)ScrollBar來改變ScrollView的位置
不過這個(gè)思路相信還是可以實(shí)現(xiàn)很多對(duì)于ScrollBar相關(guān)的需求了,最后還是貼一下demo地址ScrollBarDemo