美文网首页Android学习
可在ScrollView中自定义位置的ScrollBar

可在ScrollView中自定义位置的ScrollBar

作者: 老油条_2018 | 来源:发表于2020-06-11 18:26 被阅读0次

    前言

    目的

    在工作中经常会碰到需要自定义ScrollView的滑块位置,虽然ScrollView自身的滑块提供了如scrollBarSize等属性进行设置,但是到网上搜来搜去也没有看见能够实现ScrollView的scrollBar能够居中显示的方案,加上学的kotlin一直没有用到实战想练练手。所以,干脆自己画一个吧~

    最终效果

    ezgif-5-e09023f36d7c.gif

    ps:上图中的重新滑动不会中断动画播放的bug已修复~

    思路

    绘制

    自定义控件绘制当然是从onMeasure->onLayout->onDraw开始着手

    onMeasure

    onMeasure用于测量控件的宽高,我们的宽高直接在xml中写死即可,也不需要进行额外的测量流程,无需重写

    onLayout

    onLayout用于测量控件的位置,也是直接在xml中即可确定,我们无需重写

    onDraw

    重点就在onDraw这里,我们在绘制时需要知道如下数据

    1. 当前滑块顶部的位置top
    2. 当前滑块的高度
    3. 滑道的宽度和高度(在xml内已经固定了)
    4. 滑道的drawable(在xml内固定更好,我们先写死,后续在xml可配置)
    5. 滑块的drawable(在xml内固定更好,我们先写死,后续在xml可配置)

    知道上面的内容后,就可以实现对应的绘制功能

    class MyScrollBar(context: Context?, attrs: AttributeSet?) : View(context, attrs) {
        var mVerticalThumbHeight: Int = 100//滑块高度,先暂时写死用来看绘制效果
        var mVerticalThumbWidth: Int = 0//滑块宽度
        var mVerticalThumbTop: Int = 0//滑块当前起点位置
        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)
        }
    }
    

    计算

    何时去修改滑块位置

    1. 首次绑定时
    2. ScrollView的内部TextView内容改变时(目前只支持了TextView,其它的需要自己判断内容改变)
    3. ScrollView滚动时
    /**
     * 与ScrollView绑定
     * @param nestedScrollView 绑定的ScrollView,由于默认的ScrollView不自带滑动监听,所以此处用的是NestedScrollView
     */
    fun attachScrollView(nestedScrollView: NestedScrollView) {
        nestedScrollView.setOnScrollChangeListener { _, _, _, _, _ ->
            calculate(nestedScrollView)
        }
        val child = nestedScrollView.getChildAt(0)
        //由于一般ScrollView的子View都是TextView,这里直接在TextView的内容改变时重新测量,无需再手动监听
        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 {
            //直接调用会导致无法获取测量高度
            calculate(nestedScrollView)
        }
    }
    

    如何计算滑块位置

    private fun calculate(nestedScrollView: NestedScrollView) {
        //ScrollView的高度
        val visibleHeight = nestedScrollView.measuredHeight
        //ScrollView内部的内容高度
        val contentHeight = nestedScrollView.getChildAt(0)?.height ?: 0
        //若不需要滚动,则直接隐藏
        if (contentHeight <= visibleHeight) {
            visibility = INVISIBLE
            return
        } else {
            visibility = VISIBLE
        }
        //当前ScrollView内容滚动的距离
        val scrollY = nestedScrollView.scrollY
        //计算出滑块的高度
        mVerticalThumbHeight = measuredHeight * visibleHeight / contentHeight
        //注意滑块的top值范围是从0到{滑道高度-滑块高度}
        mVerticalThumbTop =
            (measuredHeight - mVerticalThumbHeight) * scrollY / (contentHeight - visibleHeight)
        showNow()
        invalidate()
    }
    

    实现隐藏动画

    实现动画之前明确几点

    1. 实现隐藏直接通过设置ScrollBar的alpha实现
    2. 动画通过ObjectAnimator去修改alpha值
    3. 在滑动时直接设置alpha为完全可见,并且若正在消失则取消当前动画
    4. NestedScrollView没有滑动状态改变的回调,但是可以延迟发送消失动画执行的Runnable,若期间有新滑动则取消该Runnable并重新延迟发送
        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直接调用ScrollBar的attachScrollView方法即可

    class MainActivity : AppCompatActivity() {
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main)
            scroll_bar.attachScrollView(scroll_view)
        }
    }
    
    

    总结

    自定义一个ScrollBar相对于平常的其它控件来说还是很轻松的,不会涉及到太多绘制即测量等方面的知识。因为时间比较赶,目前还缺乏以下几个功能:

    1. 实现横向滑动
    2. 通过xml配置透明度、延迟消失时间等
    3. 通过拖动ScrollBar来改变ScrollView的位置

    不过这个思路相信还是可以实现很多对于ScrollBar相关的需求了,最后还是贴一下demo地址ScrollBarDemo

    相关文章

      网友评论

        本文标题:可在ScrollView中自定义位置的ScrollBar

        本文链接:https://www.haomeiwen.com/subject/xicitktx.html