android:scrollbars 滑动滚动条代码控制

Android中的View对于ScrollBar和Scroll的支持是非常灵活的,不仅仅是UI样式可变,计算参数的方式也是可变的。


在Android中,任何View都可以显示出ScrollBar,唯一的条件是自身高度不足以显示全部内容。

在UI元素上,ScrollBar由两部分组成,一个是Track(滑道),一个是Thumb(滑块),这两部分都是可以定制的(指定自定义的drawable),另外ScrollBar的宽度(竖向)或高度(横向)也是可以控制的,相关的控制属性是:

滚动条不显示:android:scrollbars="none"
滚动条恒显示:android:fadeScrollbars="false"
设置水平滚动条的drawable(如颜色):android:scrollbarThumbHorizontal
设置垂直滚动条的drawable(如颜色):android:scrollbarThumbVertical
设置水平滚动条背景(轨迹)的色drawable(如颜色):android:scrollbarTrackHorizontal
设定滚动条宽度:android:scrollbarSize


ScrollBar分为竖向的和横向的,也可以强制去掉,控制这一点的属性是:android:scrollbars

对于竖向的ScrollBar,我们还可以控制它是显示在左边还是右边,控制这一点的函数是:setVerticalScrollbarPosition

ScrollBar还有淡出效果,在时间参数和是否允许淡出方面,我们是可以控制的:

android:fadeScrollbars
android:scrollbarDefaultDelayBeforeFade
android:scrollbarFadeDuration

在ScrollBar淡出之后,ScrollBar一般是完全看不见的,但是我们可以选择Track始终可见:

android:scrollbarAlwaysDrawHorizontalTrack

android:scrollbarAlwaysDrawVerticalTrack

android:isScrollContainer是一个令人迷惑的属性,设置它或者不设置它,有时并不能带来明显的区别。如果设置为true且它的子View包含EditText,并且连接到输入法(此时软键盘会弹出 ),那么软键盘会尽最大可能挤压该View,那样的话,该View的整个内容都会出现在软键盘之上,而不是部分内容被遮住。但是即使不设置该属性,有时仍然能达到上述效果,这是因为 Android在挤压View的时候,也会考虑其它因素,设置该属性可以在这方面得到一个保证。下面的问答很好的解释了这个属性的意义和原理:
view - What does android:isScrollContainer do? - Stack Overflow

 android:scrollbarStyle控制着ScrollBar的显示位置和样式,可取的值如下:

insideOverlay
insideInset
outsideOverlay
outsideInset

    inside表示显示在padding区域的内侧,outside表示显示在padding区域的外侧。
    Inset表示将自动增加padding以显示ScrollBar(这意味着内容区域将缩小),Overlay表示不会增加padding以显示ScrollBar,而是浮动在内容上面(可能会遮住内容)。

ListView:

第一

stackFromBottom属性, 设置该属性之后你做好的列表在显示的时候会显示列表的最后几条信息,滚动条也会滚到最下面,值为true和false

android:stackFromBottom="true" 显示最后几条,默认为false。

第二

transciptMode属性,需要用ListView或者其它显示大量Items的控件实时跟踪或者查看信息,并且希望最新的条目可以自动滚动到可视范围内。通过设置的控件transcriptMode属性可以将Android平台的控件(支持ScrollBar)自动滑动到最底部。

android:transcriptMode="alwaysScroll"

第三

cacheColorHint属性,很多人希望能够改变一下它的背景,使他能够符合整体的UI设计,改变背景背很简单只需要准备一张图片然后指定属性 android:background="@drawable/bg",不过不要高兴地太早,当你这么做以后,发现背景是变了,但是当你拖动,或者点击list空白位置的时候发现ListItem都变成黑色的了,破坏了整体效果。

如果你只是换背景的颜色的话,可以直接指定android:cacheColorHint为你所要的颜色,如果你是用图片做背景的话,那也只要将android:cacheColorHint指定为透明(#00000000)就可以了

第四

divider属性,该属性作用是每一项之间需要设置一个图片做为间隔,或是去掉item之间的分割线

android:divider="@drawable/list_driver" 其中 @drawable/list_driver 是一个图片资源,如果不想显示分割线则只要设置为android:divider="@drawable/@null" 就可以了(原著是错误的,在eclipse @drawable/@null下无法编译,应该改为android:divider="#00000000")

第五

fadingEdge属性,上边和下边有黑色的阴影

android:fadingEdge="none" 设置后没有阴影了

第六

scrollbars属性,作用是隐藏listView的滚动条,

android:scrollbars="none"与setVerticalScrollBarEnabled(true);的效果是一样的,不活动的时候隐藏,活动的时候也隐藏

第七

fadeScrollbars属性,android:fadeScrollbars="true" 配置ListView布局的时候,设置这个属性为true就可以实现滚动条的自动隐藏和显示。

以下是我自己添加的

   第八

   android:scrollbarThumbHorizontal   设置水平滚动条的drawable(如颜色)。

   android:scrollbarTrackHorizonta    设置水平滚动条背景(轨迹)的色drawable(如颜色)。

   第九

   android:scrollbarThumbVertical     设置垂直滚动条的drawable(如颜色)。

  android:scrollbarTrackVertical    设置垂直滚动条背景(轨迹)的drawable。
 


Android上有一些控件,像listview, ScrollView滚动的时候右侧会出现一个滚动条,如何隐藏掉呢......

代码中:
setScrollbarFadingEnabled(true);//不活动的时候隐藏,活动的时候显示

setVerticalScrollBarEnabled(true);//不活动的时候隐藏,活动的时候也隐藏


xml中配置:

<ScrollView  

            android:layout_width="fill_parent"
            android:layout_height="wrap_content"

            android:scrollbars="none">
       <TextView

                android:id="@+id/showhtml"

                android:layout_width="fill_parent"
                android:layout_height="wrap_content" />
</ScrollView>

其中 android:scrollbars="none"与setVerticalScrollBarEnabled(true);效果一样。


更多:

Android必知必会-自定义Scrollbar样式_android 自定义scrollbar样式-CSDN博客


修改ScrollView滚动条样式

在ListView/ScrollView/RecyclerView中添加属性:

<!-- 情况A :垂直滚动条-->
android:scrollbars="vertical"
android:scrollbarTrackVertical="@drawable/xxx_vertical_track"
android:scrollbarThumbVertical="@drawable/xxx_vertical_thumb"
<!-- 情况B :水平滚动条-->
android:scrollbars="horizontal"
android:scrollbarTrackHorizontal="@drawable/xxx_horizontal_track"
android:scrollbarThumbHorizontal="@drawable/xxx_horizontal_thumb"

<!-- 其他通用的属性 -->
<!-- 1.定义滚动条的样式和位置 -->
android:scrollbarStyle="outsideInset"
<!-- 2.定义滚动条的大小,垂直时指宽度,水平时指高度 -->
android:scrollbarSize="4dp"

即scrollbaTrackxxx,scrollbarThumbxxx自定义的 xml 文件,放在Drawable中,track是指长条,thumb是指短条,然后再 xml 中定义短条和长条的样式。

需要注意
其中,scrollbaTrackxxx、scrollbarThumbxxx可以使用:

  • Shape自定义 Drawable
  • 图片
  • .9.png
  • @color/xxx的方式使用颜色值

不可以直接使用#xxxxxx颜色值

android:scrollbarStyle

android:scrollbarStyle可以定义滚动条的样式和位置,可选值有insideOverlay、insideInset、outsideOverlay、outsideInset四种。

其中inside和outside分别表示是否在 view 的 padding 区域内,overlay和inset表示覆盖在 view 上或是插在 view 后面,所以四种值分别表示:


自定义ScrollView滑动条(带隐藏动画、自定义位置的ScrollBar)

简介

相信你也使用过滚动视图scrollview,这也是一个比较常见并且实用的view。但是不知道你是否遇到了一个问题,带滚动功能的view官方都为我们定义了自带的ScrollBar,也就是滑动条。利用官方所给的属性,我们能对其做一些比较基础的设置。
例如:
滚动方向

<!-- Defines which scrollbars should be displayed on scrolling or not. -->
        <attr name="scrollbars">
            <!-- No scrollbar is displayed. -->
            <flag name="none" value="0x00000000" />
            <!-- Displays horizontal scrollbar only. -->
            <flag name="horizontal" value="0x00000100" />
            <!-- Displays vertical scrollbar only. -->
            <flag name="vertical" value="0x00000200" />
        </attr>

滚动条、轨迹样式

<!-- Defines the horizontal scrollbar thumb drawable. -->
        <attr name="scrollbarThumbHorizontal" format="reference" />
        <!-- Defines the vertical scrollbar thumb drawable. -->
        <attr name="scrollbarThumbVertical" format="reference" />
        <!-- Defines the horizontal scrollbar track drawable. -->
        <attr name="scrollbarTrackHorizontal" format="reference" />
        <!-- Defines the vertical scrollbar track drawable. -->
        <attr name="scrollbarTrackVertical" format="reference" />
        <!-- Defines whether the horizontal scrollbar track should always be drawn. -->

布局方式

<!-- Controls the scrollbar style and position. The scrollbars can be overlaid or
             inset. When inset, they add to the padding of the view. And the
             scrollbars can be drawn inside the padding area or on the edge of
             the view. For example, if a view has a background drawable and you
             want to draw the scrollbars inside the padding specified by the
             drawable, you can use insideOverlay or insideInset. If you want them
             to appear at the edge of the view, ignoring the padding, then you can
             use outsideOverlay or outsideInset.-->
        <attr name="scrollbarStyle">
            <!-- Inside the padding and overlaid. -->
            <enum name="insideOverlay" value="0x0" />
            <!-- Inside the padding and inset. -->
            <enum name="insideInset" value="0x01000000" />
            <!-- Edge of the view and overlaid. -->
            <enum name="outsideOverlay" value="0x02000000" />
            <!-- Edge of the view and inset. -->
            <enum name="outsideInset" value="0x03000000" />
        </attr>

其中比较值得探讨的则是布局方式这一属性。网上有很多关于这个属性的介绍, 具体分析可以参考这位博主的文章,我不再过多阐述。


本文参考此篇文章:https://www.cnblogs.com/tangZH/p/8423803.html
 

代码
主要自定义代码
继承自ScrollView的EditScrollBarScrollView

/**
 * 可编辑垂直滑动条的ScrollView
 * scrollBarTopAndBottom:           滑动条上下留白距离
 * scrollBarWidth:                  滑动条的宽度
 * scrollBarHeight:                 滑动条的高度
 * scrollBarPaddingRight:           滑动条距离右侧的距离
 * scrollBarSrc:                    滑动条图片资源,需要设置大小
 * isScrollBarPlayHiddenAnimation:  是否播放隐藏动画
 * hiddenAnimationUseTime:          隐藏动画播放时间(ms)50ms以上,建议为50ms倍数
 * delayHideTime:                   延迟隐藏时间
 * maxHeight:                       layout_height为wrap_content时最大高度
 * 以上均可不传入使用默认值
 */
@SuppressLint("CustomViewStyleable")
class EditScrollBarScrollView @JvmOverloads constructor(
    context: Context,
    attributeSet: AttributeSet? = null
) : ScrollView(context, attributeSet) {
    private var saveDistance = 0//滑动标记
    private var scrollBarTopAndBottom = resources.getDimension(R.dimen.dp_28)//滑动条上下留白距离
    private var scrollBarHeight = resources.getDimension(R.dimen.dp_122)//滑动条的高度
    private var scrollBarWidth = resources.getDimension(R.dimen.dp_4)//滑动条的宽度
    private var scrollBarPaddingRight = resources.getDimension(R.dimen.dp_12)//滑动条距离右侧的距离
    private var scrollBarPos = 0.0f//滑动条定位 绘制的关键所在
    private var mAllHeight = 0.0f//内部视图的高度
    private var mHeight = 0.0f//ScrollView高度
    private var mWidth = 0.0f//ScrollView宽度
    private var yPro = 0.0f//滑动比例
    private lateinit var scrollBarSrc: Bitmap//资源
    private var isScrollBarPlayHiddenAnimation = true//是否播放隐藏动画
    private var hiddenAnimationUseTime = 500f//隐藏动画播放时间(ms)50ms以上,建议为50ms倍数
    private var delayHideTime = 1000L//延迟隐藏时间
    private var mMaxHeight = 0.0f//最大高度
    private var animationTimeInterval = 50L//时间间隔
    private var startPlayAnimationTime = System.currentTimeMillis()//开始播放的时间戳
    private var runTotalTime = 0//需执行次数
    private var runTime = 1//执行次数记录

    private var timer = Timer()//定时器
    private lateinit var playTask: TimerTask //播放任务

    private val mPaint: Paint = Paint()// 画笔

    init {
        isVerticalScrollBarEnabled = false//设置原有的滚动无效
        if (attributeSet != null) {
            val array: TypedArray =
                context.obtainStyledAttributes(attributeSet, R.styleable.EditScrollBarScrollView)

            //取得xml设置属性
            scrollBarTopAndBottom =
                array.getDimension(R.styleable.EditScrollBarScrollView_scrollBarTopAndBottom, 0F)
            scrollBarHeight =
                array.getDimension(R.styleable.EditScrollBarScrollView_scrollBarHeight, 0F)
            scrollBarWidth =
                array.getDimension(R.styleable.EditScrollBarScrollView_scrollBarWidth, 0F)
            scrollBarPaddingRight =
                array.getDimension(R.styleable.EditScrollBarScrollView_scrollBarPaddingRight, 0F)
            isScrollBarPlayHiddenAnimation = array.getBoolean(
                R.styleable.EditScrollBarScrollView_isScrollBarPlayHiddenAnimation,
                true
            )
            var starFullId =
                array.getResourceId(R.styleable.EditScrollBarScrollView_scrollBarSrc, 0)
            hiddenAnimationUseTime =
                array.getFloat(R.styleable.EditScrollBarScrollView_hiddenAnimationUseTime, 500f)
            delayHideTime =
                array.getFloat(R.styleable.EditScrollBarScrollView_delayHideTime, 1000f).toLong()
            mMaxHeight = array.getDimension(R.styleable.EditScrollBarScrollView_maxHeight, 0f)

            //传入默认值
            if (scrollBarTopAndBottom == 0f) {
                scrollBarTopAndBottom = resources.getDimension(R.dimen.dp_28)
            }
            if (scrollBarHeight == 0f) {
                scrollBarHeight = resources.getDimension(R.dimen.dp_122)
            }
            if (scrollBarWidth == 0f) {
                scrollBarWidth = resources.getDimension(R.dimen.dp_4)
            }
            if (scrollBarPaddingRight == 0f) {
                scrollBarPaddingRight = resources.getDimension(R.dimen.dp_12)
            }

            if (starFullId == 0) {
                LogUtil.d(TAG, "未设置滑动条图片资源,使用默认资源")
                starFullId = R.drawable.bg_scrollbar
            }

            //设置背景
            val drawable = getDrawable(context, starFullId)
            scrollBarSrc = Bitmap.createBitmap(
                drawable!!.intrinsicWidth,
                drawable.intrinsicHeight,
                Bitmap.Config.ARGB_8888
            )
            val mCanvas = Canvas(scrollBarSrc)
            drawable.setBounds(0, 0, mCanvas.width, mCanvas.height)
            drawable.draw(mCanvas)

            //设置宽高
            scrollBarSrc = resetBitmap(scrollBarSrc, scrollBarWidth, scrollBarHeight)

            scrollBarPos = scrollBarTopAndBottom

            playTask = PlayTask()

            runTotalTime =
                (hiddenAnimationUseTime / animationTimeInterval).toInt() + 2//多执行几次保证滑动条完全消失

            if (isScrollBarPlayHiddenAnimation) {
                mPaint.alpha = 0//第一次刷新时不显示滑动条
            }

            array.recycle()
        }
    }

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        //测量ScrollView
        mHeight = MeasureSpec.getSize(heightMeasureSpec).toFloat()
        mWidth = MeasureSpec.getSize(widthMeasureSpec).toFloat()
        if (childCount > 0) {
            //测量子视图
            mAllHeight = getChildAt(0).measuredHeight.toFloat()
            //计算滑动比例
            val rangeBar = mHeight - scrollBarHeight - scrollBarTopAndBottom * 2
            val rangeScrollView = mAllHeight - mHeight + paddingTop + paddingBottom
            yPro = rangeBar / rangeScrollView
        }
        if (mMaxHeight != 0f && mAllHeight > mMaxHeight) {
            LogUtil.d(TAG, "maxHeight = $mMaxHeight")
            val height = mMaxHeight.toInt()
            val width = mWidth.toInt()
            //设置最大高度
            setMeasuredDimension(width, height)
            //重新计算滑动比例
            val rangeBar = height - scrollBarHeight - scrollBarTopAndBottom * 2
            val rangeScrollView = mAllHeight - height + paddingTop + paddingBottom
            yPro = rangeBar / rangeScrollView
        }
        LogUtil.d(
            TAG,
            "测量 mWidth = $mWidth ,mHeight = $mHeight ,mAllHeight = $mAllHeight ,yPro = $yPro ,scrollBarPos = $scrollBarPos ,paddingTop = $paddingTop ,paddingBottom = $paddingBottom"
        )
    }override fun onDraw(canvas: Canvas?) {
        super.onDraw(canvas)
        canvas?.drawBitmap(
            scrollBarSrc,
            mWidth - scrollBarPaddingRight - scrollBarWidth,
            scrollBarPos,
            mPaint
        )
    }

    override fun onScrollChanged(l: Int, t: Int, oldl: Int, oldt: Int) {
        super.onScrollChanged(l, t, oldl, oldt)
        if (isScrollBarPlayHiddenAnimation) {
            resetStatus()
        }
        //当快速抛动,手指离开屏幕时,就会出现相邻的两次相同位置重复出现的情况,这种情况下不要去绘制滑动条
        if (saveDistance == t) {
            return
        } else
            saveDistance = t
        //滑动条的上边缘y坐标应该为ScrollView滑动的距离乘以滑动条的滑动比例,加上ScrollView滑动的距离(因为滑动条也会跟着滑动,所以应该抵消滑动条被带着滑动的距离)
        scrollBarPos += ((t - oldt) + (t - oldt) * yPro)
        //重新绘制
        invalidate()
    }

    /**
     * 如果用户设置了图片的宽高,就重新设置图片
     */
    private fun resetBitmap(bitMap: Bitmap, width: Float, height: Float): Bitmap {
        // 得到新的图片
        return Bitmap.createScaledBitmap(bitMap, width.toInt(), height.toInt(), true)
    }

    /**
     * 重绘ScrollBar
     */
    private fun reDrawScrollBar() {
        if (runTime == 1) {
            startPlayAnimationTime = System.currentTimeMillis()
        }
        if (runTime >= runTotalTime) {
            clearTimer()
            return
        }
        val pro =
            (hiddenAnimationUseTime - (System.currentTimeMillis() - startPlayAnimationTime)) / hiddenAnimationUseTime
        LogUtil.d(TAG, "差值${System.currentTimeMillis() - startPlayAnimationTime}")
        if (pro in 0f..1f) {
            mPaint.alpha = (255 * pro).toInt()
        } else if (pro < 0) {
            mPaint.alpha = 0
        } else {
            mPaint.alpha = 255
        }
        LogUtil.d(TAG, "执行次数$runTime,比例$pro,透明度${mPaint.alpha}")
        runTime++
        postInvalidate()
    }

    private fun resetStatus() {
        mPaint.alpha = 255
        runTime = 1

        clearTimer()

        setTimer()

        timer.scheduleAtFixedRate(playTask, delayHideTime, animationTimeInterval)
    }

    private fun clearTimer() {
        playTask.cancel()
        timer.cancel()
        timer.purge()
    }

    private fun setTimer() {
        timer = Timer()
        playTask = PlayTask()
    }

    inner class PlayTask : TimerTask() {
        override fun run() {
            reDrawScrollBar()
        }
    }

    companion object {
        private val TAG = EditScrollBarScrollView::class.java.name
    }
}

自定义属性
app/src/main/res/value/attr.xml
中添加如下字段
注意maxHeight并非安卓预定义的maxHeight,在此处为我们自定义的属性。

	<declare-styleable name="EditScrollBarScrollView">
        <!--滑动条上下留白距离-->
        <attr name="scrollBarTopAndBottom" format="dimension" />
        <!--滑动条的宽度-->
        <attr name="scrollBarWidth" format="dimension" />
        <!--滑动条的高度-->
        <attr name="scrollBarHeight" format="dimension" />
        <!--滑动条距离右侧的距离-->
        <attr name="scrollBarPaddingRight" format="dimension" />
        <!--滑动条图片资源,需要设置大小-->
        <attr name="scrollBarSrc" format="reference" />
        <!--是否展示隐藏动画-->
        <attr name="isScrollBarPlayHiddenAnimation" format="boolean" />
        <!--隐藏动画播放时间(ms)50ms以上,建议为50ms倍数-->
        <attr name="hiddenAnimationUseTime" format="float" />
        <!--延迟隐藏时间-->
        <attr name="delayHideTime" format="float" />
        <!--layout_height为wrap_content时最大高度-->
        <attr name="maxHeight" format="dimension" />
    </declare-styleable>	

布局

在所需的activity父布局中添加如下代码

<!--以上自定义属性均可以加入并设置value 具体注意事项可自行测试或根据注释理解-->

<!--以上自定义属性均可以加入并设置value 具体注意事项可自行测试或根据注释理解-->
<com.konka.yilife.views.EditScrollBarScrollView
            android:layout_width="xdp"
            android:layout_height="xdp"
            android:paddingTop="@dimen/dp_4"
            android:paddingBottom="@dimen/dp_4">

            <androidx.constraintlayout.widget.ConstraintLayout
                android:layout_width="wrap_content"
                android:layout_height="wrap_content">
                
                <!--添加你的具体布局内容-->

            </androidx.constraintlayout.widget.ConstraintLayout>
</com.konka.yilife.views.EditScrollBarScrollView>


原理实现


相信作为安卓开发工程师,你也接触过自定义View。自定义View的知识是安卓很重要也很有实际意义的一块内容,如果你稍微对此有些了解,那么你应该知道,自定义View的主要步骤是测量和绘制。


这要牵扯到上文中的两个方法onMeasure和onDraw,至于在这两个方法之前的初始化过程,我主要强调一下自定义属性的使用:

scrollBarTopAndBottom =
                array.getDimension(R.styleable.EditScrollBarScrollView_scrollBarTopAndBottom, 0F)

上文中的R.styleable.EditScrollBarScrollView_scrollBarTopAndBottom就是我们在自定义属性declare-styleable中定义的scrollBarTopAndBottom,并且由AndroidStudio为我们生成的特定名称。自定义属性在定义时format了什么类型,就会有对应的方法来获取这个xml传入的value值。例如format传入float,代码中使用array.getFloat方法获取对应value。具体可以参考这篇文章介绍。
初始化过程结束后,记得调用方法array.recycle(),这个方法的含义在于xml中array的取得使用到了SynchronizedPool对象缓存池。调用此方法能够释放内存,避免了OutOfMemory的发生。可以参考这边文章。
其余我则不多做赘述,相信你有基本的语言常识,初始化的过程结合注释你都可以大概理解。

测量

我们先来深入探究一下onMeasure方法,首先我们可以将上文onMeasure的代码注释掉,替换以下:

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        //测量ScrollView
        mHeight = MeasureSpec.getSize(heightMeasureSpec).toFloat()
        mWidth = MeasureSpec.getSize(widthMeasureSpec).toFloat()
        if (childCount > 0) {
            //测量子视图
            mAllHeight = getChildAt(0).measuredHeight.toFloat()
            //计算滑动比例
            val rangeBar = mHeight - scrollBarHeight - scrollBarTopAndBottom * 2
            val rangeScrollView = mAllHeight - mHeight
            yPro = rangeBar / rangeScrollView
        }
        LogUtil.d(
            TAG,
            "测量 mWidth = $mWidth ,mHeight = $mHeight ,yPro = $yPro ,scrollBarPos = $scrollBarPos"
        )
    }


测量关键数据


观察区别可以看到,我们去掉了关于padding属性、maxHeight属性的逻辑。抽取出了测量的主要逻辑。
首先我们需要明确,ScrollVIew的子View最好定义为一个,此处我的使用方法是将子View定义为一个ViewGroup,即上文的ConstraintLayout。
通过MeasureSpec.getSize和传入的两个值测量ScrollView的宽高mHeight和mWidth
通过getChildAt(0).measuredHeight测量子View的高度mAllHeight
1、现在我们已经拿到了最重要的数据,接下来理解一下滑动比例的计算方法。
2、我们首先将子View在ScrollView中上下滑动的过程,相对的想像为ScrollView在子View上滑动的过程。
那么ScrollView看作一个窗口,里面的子View为一条固定长度的轨道,窗口在轨道上平滑的移动,每次只能看到窗口大小的轨道,其余窗口外的轨道均无法看见。
3、现在我们需要做的事情很简单,只是将滑动条(小窗口)在滑动轨道(小轨道)上的滑动,按照比例映射到ScrollView(大窗口)在子View(大轨道)上滑动的过程。那么仔细想想,我们只需要计算滑动条能在滑动轨道上滑动宽度(宽度1) 与 ScrollView能在子View上滑动宽度(宽度2) 的 比例即可。
4、知道了大概思路,我们只需要用获取到的数据计算即可。首先是宽度1,因为在自定义属性中我们设置了滑动条距离ScrollView上下边的边距,所以我们将ScrollView的长度mHeight减去2*scrollBarTopAndBottom,为滑动轨道的实际长度,考虑到滑动条本身长度,其能在轨道上滑动的距离是在开始处某一点的位置,到滑动到结束处该同一点位置之间的差值,所以我们再使用上述滑动轨道的实际长度减去滑动条本身长度scrollBarHeight,如此得到宽度1;然后思考宽度2,同理应该是用子View的长度mAllHeight减去 ScrollView的长度 mHeight,如果此时考虑到添加了padding属性,那么按照宽度1的计算过程,同样是有上下的边距值,则ScrollView的长度应该减去边距值paddingTop + paddingBottom,也就是总体加上多减去的两个边距值,如此得到宽度2。再将两者求比例即可获取这个关键的比例值。这里需要注意,不要混淆了scrollBarTopAndBottom和上下的padding值,两个虽然都是在ScrollView上的边距值,但是针对了不同的对象。
也即

val rangeBar = mHeight - scrollBarHeight - scrollBarTopAndBottom * 2
//-(mHeight - (paddingTop + paddingBottom))
val rangeScrollView = mAllHeight - mHeight + paddingTop + paddingBottom
yPro = rangeBar / rangeScrollView

添加maxHeight属性


上文中我们还去掉了关于maxHeight的逻辑,那么不妨来分析分析,这个属性的意义和具体的实现方法。


为什么要添加这样一条自定义属性呢,我们在使用某些View时,也是接触过这个属性的。他的所用就是对View的宽度或者长度做一个限制,能够满足一些特定的需求。

例如,我们有一个界面,需要显示一个文本,文本放置在一个容器中。容器下方有其他的组件排列。我们需要让文本容器有一个最大的高度,文字超出这个高度就可以在容器中单独滚动。其余的组件则是在整个大的界面上可以滚动显示。
设想容器的高度为固定值:文字很少时,我们的容器高度过高,出现大量留白;文字很多时,容器无法显示完全部的内容。
当然你会想到,用一个ScrollView作为容器,并将高度设置为wrap_content,并规定maxHeight属性即可。但是不妨实际试试,你会发现,maxHeight属性在wrap_content属性下完全不起作用,ScrollView会完全包裹住子View,失去滚动效果。这是由于onMeasure方法中设置了了一些参数,使高度按照一些比较特殊的方式来测量。

        /**
         * Measure specification mode: The parent has not imposed any constraint
         * on the child. It can be whatever size it wants.
         */
        public static final int UNSPECIFIED = 0 << MODE_SHIFT;

        /**
         * Measure specification mode: The parent has determined an exact size
         * for the child. The child is going to be given those bounds regardless
         * of how big it wants to be.
         */
        public static final int EXACTLY     = 1 << MODE_SHIFT;

        /**
         * Measure specification mode: The child can be as large as it wants up
         * to the specified size.
         */
        public static final int AT_MOST     = 2 << MODE_SHIFT;


此处不多做赘述,仅仅列举三种测量模式,感兴趣的可以自行参考网上相关的文章。我们继续说回实现方案。
既然自带的maxHeight不起作用,那么我们不妨自行定义一个maxHeight属性。同理在init方法中引入作为全局变量保存。
关键看这一部分代码

        if (mMaxHeight != 0f && mAllHeight > mMaxHeight) {
            LogUtil.d(TAG, "maxHeight = $mMaxHeight")
            val height = mMaxHeight.toInt()
            val width = mWidth.toInt()
            //设置最大高度
            setMeasuredDimension(width, height)
            //重新计算滑动比例
            val rangeBar = height - scrollBarHeight - scrollBarTopAndBottom * 2
            val rangeScrollView = mAllHeight - height + paddingTop + paddingBottom
            yPro = rangeBar / rangeScrollView
        }

仔细观察可以发现其实我们做的事情,只是在上述操作之后,判断到设置了最大高度并且其比子View的高度长。在此条件下将上述的ScrollView的高度替换为设置的最大高度代入同样的计算过程。并且调用了setMeasuredDimension方法重新设置ScrollView的宽高。

参考图片理解

滚动监听


在测量完毕需要的数据后,我们很容易想到,以此刻的数据,无法绘制出某一刻具体的位置,因为我们只能在子View滑动到某一处(位置信息)时,通过求取到的比例来计算此刻滑动条的位置。
这里介绍一个ScrollView的自带的一个监听方法↓

android.view.View protected void onScrollChanged(int l,
                               int t,
                               int oldl,
                               int oldt)
This is called in response to an internal scroll in this view (i.e., the view scrolled its own contents). 
This is typically as a result of scrollBy(int, int) or scrollTo(int, int) having been called.

Params:
l – Current horizontal scroll origin.
t – Current vertical scroll origin.
oldl – Previous horizontal scroll origin.
oldt – Previous vertical scroll origin.

这个监听方法会有一些问题,具体也在注释中有相关说明,一个判断语句和一个全局变量就可以将这个问题解决。
再将上文的方法复制下来做一个分析。

    override fun onScrollChanged(l: Int, t: Int, oldl: Int, oldt: Int) {
        super.onScrollChanged(l, t, oldl, oldt)
        if (isScrollBarPlayHiddenAnimation) {
            //是否播放隐藏动画,这里可以先不做考虑
            resetStatus()
        }
        //当快速抛动,手指离开屏幕时,就会出现相邻的两次相同位置重复出现的情况,这种情况下不要去绘制滑动条
        if (saveDistance == t) {
            return
        } else
            saveDistance = t
        //滑动条的上边缘y坐标应该为ScrollView滑动的距离乘以滑动条的滑动比例,加上ScrollView滑动的距离(因为滑动条也会跟着滑动,所以应该抵消滑动条被带着滑动的距离)
        scrollBarPos += ((t - oldt) + (t - oldt) * yPro)
        //重新绘制
        invalidate()
    }


可以看到监听方法中有两个值t – Current vertical scroll origin. 和 oldt – Previous vertical scroll origin. 字面意思就是子View上次和本次的垂直位置。t-oldt则是滑动的改变值,以这个值乘以比例yPro则获得了滑动条实际需要滑动的距离。
但是可以看到,代码中在此基础上加了t-oldt,这是为什么呢。我们需要明白滑动条是放置在绘制的Canvas中的一个固定的物体而已,他在绘制出来后,位置就是固定的,并不会脱离整个Canvas单独改变位置。我们在滑动子View时,也需要考虑到滑动条自身也在滑动,所以要将滑动所改变的距离加上,这样视觉上就可以造成我们的滑动条与子View是分离的效果。
而scrollBarPos则是我们声明的全局变量,代表了滑动条上端点的位置值,其初始化是在init函数初始化scrollBarTopAndBottom值之后,将其值作为初始的位置。
当然别忘了invalidate(),在主线程中通知到数据改变,重新调用到onDraw方法绘制我们的滑动条。
那么到这里,我们所需要的数据都拿到了,接下来就是绘制了。

绘制


onDraw方法很简单,就是绘制一个bitmap在canvas中,用上我们计算得到的数据即可。联系到我们在滑动监听中一直在改变scrollBarPos的值,并且调用invalidate()方法去通知调用重绘方法onDraw,那么到这里,一个动态滚动的滑动条才是真正的实现出来。

    override fun onDraw(canvas: Canvas?) {
        super.onDraw(canvas)
        canvas?.drawBitmap(
            scrollBarSrc,
            mWidth - scrollBarPaddingRight - scrollBarWidth,
            scrollBarPos,
            mPaint
        )
    }


需要注意的是drawBitmap方法的参数,分别是图片资源、左端点位置、上端点位置、画笔。

渐隐效果
你如果仔细阅读了我的代码,这个效果应该是很容易实现的,相信你也有了自己的思考,这里简述我的做法。
考虑到这是一个绘制的过程,隐藏的效果也不一定是将其移除,我通过画笔的透明度属性来实现这个功能。

    /**
     * 重绘ScrollBar
     */
    private fun reDrawScrollBar() {
        if (runTime == 1) {
            startPlayAnimationTime = System.currentTimeMillis()
        }
        if (runTime >= runTotalTime) {
            clearTimer()
            return
        }
        val pro =
            (hiddenAnimationUseTime - (System.currentTimeMillis() - startPlayAnimationTime)) / hiddenAnimationUseTime
        LogUtil.d(TAG, "差值${System.currentTimeMillis() - startPlayAnimationTime}")
        if (pro in 0f..1f) {
            mPaint.alpha = (255 * pro).toInt()
        } else if (pro < 0) {
            mPaint.alpha = 0
        } else {
            mPaint.alpha = 255
        }
        LogUtil.d(TAG, "执行次数$runTime,比例$pro,透明度${mPaint.alpha}")
        runTime++
        postInvalidate()
    }


我定义了一些属性,用来规定延迟隐藏的时间、整个隐藏动画的时间。这样在xml中可以手动的更改数值来调整自己想要的效果。
固定了每一次重绘的间隔时间为50ms,那么用总时间除以他获取需要重绘的次数。

runTotalTime =
                (hiddenAnimationUseTime / animationTimeInterval).toInt() + 2//多执行几次保证滑动条完全消失


透明度按照时间比例来计算,每次使用总的播放时间减去当前时间和开始时间的差值计算还剩下多少播放时间,以剩余播放时间除以总时间获取当前时间滑动条应该使用的透明度。
注意使用postInvalidate()方法,因为使用Timer是开启了线程,并非在UI线程中执行更新UI,需要避免线程不安全问题。
主要使用到了Timer类和TimerTask类来作为计时器。

    private fun resetStatus() {
        mPaint.alpha = 255
        runTime = 1

        clearTimer()

        setTimer()

        timer.scheduleAtFixedRate(playTask, delayHideTime, animationTimeInterval)
    }

    private fun clearTimer() {
        playTask.cancel()
        timer.cancel()
        timer.purge()
    }

    private fun setTimer() {
        timer = Timer()
        playTask = PlayTask()
    }

    inner class PlayTask : TimerTask() {
        override fun run() {
            reDrawScrollBar()
        }
    }

方法scheduleAtFixedRate()和schedule()功能相似,使用方法也类似。
前者更注重每一次执行的时间和第一次执行的间隔接近规定值,换一句话说就是更加注重频率。
后者更注重每一次执行的时间和上一次执行的间隔接近规定值,换言之更加注重周期。
因为执行次数很少,在这里用哪一种都无所谓。但是为了让消失的动画更加平滑,我们采用前者。
至此,整个自定义ScrollView中的ScrollBar的功能就全部实现了。本文参考了多篇文章,侵权必删。

  • 29
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值