可在ScrollView中自定義位置的ScrollBar

前言

目的

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

最終效果

ezgif-5-e09023f36d7c.gif

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ù)

  1. 當(dāng)前滑塊頂部的位置top
  2. 當(dāng)前滑塊的高度
  3. 滑道的寬度和高度(在xml內(nèi)已經(jīng)固定了)
  4. 滑道的drawable(在xml內(nèi)固定更好,我們先寫死,后續(xù)在xml可配置)
  5. 滑塊的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í)去修改滑塊位置

  1. 首次綁定時(shí)
  2. ScrollView的內(nèi)部TextView內(nèi)容改變時(shí)(目前只支持了TextView,其它的需要自己判斷內(nèi)容改變)
  3. 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)

  1. 實(shí)現(xiàn)隱藏直接通過設(shè)置ScrollBar的alpha實(shí)現(xiàn)
  2. 動(dòng)畫通過ObjectAnimator去修改alpha值
  3. 在滑動(dòng)時(shí)直接設(shè)置alpha為完全可見,并且若正在消失則取消當(dāng)前動(dòng)畫
  4. 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è)功能:

  1. 實(shí)現(xiàn)橫向滑動(dòng)
  2. 通過xml配置透明度、延遲消失時(shí)間等
  3. 通過拖動(dòng)ScrollBar來改變ScrollView的位置

不過這個(gè)思路相信還是可以實(shí)現(xiàn)很多對(duì)于ScrollBar相關(guān)的需求了,最后還是貼一下demo地址ScrollBarDemo

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容