行业知识
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:fadeScrollbarsandroid:scrollbarDefaultDelayBeforeFadeandroid: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的显示位置和样式,可取的值如下:
insideOverlayinsideInsetoutsideOverlayoutsideInset
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)就可以了
第四
pider属性,该属性作用是每一项之间需要设置一个图片做为间隔,或是去掉item之间的分割线
android:pider="@drawable/list_driver" 其中 @drawable/list_driver 是一个图片资源,如果不想显示分割线则只要设置为android:pider="@drawable/@null" 就可以了(原著是错误的,在eclipse @drawable/@null下无法编译,应该改为android:pider="#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中配置:
其中 android:scrollbars="none"与setVerticalScrollBarEnabled(true);效果一样。
更多:
Android必知必会-自定义Scrollbar样式_android 自定义scrollbar样式-CSDN博客
修改ScrollView滚动条样式
在ListView/ScrollView/RecyclerView中添加属性:
android:scrollbars="vertical"android:scrollbarTrackVertical="@drawable/xxx_vertical_track"android:scrollbarThumbVertical="@drawable/xxx_vertical_thumb"android:scrollbars="horizontal"android:scrollbarTrackHorizontal="@drawable/xxx_horizontal_track"android:scrollbarThumbHorizontal="@drawable/xxx_horizontal_thumb"android:scrollbarStyle="outsideInset"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,也就是滑动条。利用官方所给的属性,我们能对其做一些比较基础的设置。
例如:
滚动方向
滚动条、轨迹样式
布局方式
其中比较值得探讨的则是布局方式这一属性。网上有很多关于这个属性的介绍, 具体分析可以参考这位博主的文章,我不再过多阐述。
本文参考此篇文章: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自定义属性
app/src/main/res/value/attr.xml
中添加如下字段
注意maxHeight并非安卓预定义的maxHeight,在此处为我们自定义的属性。
布局
在所需的activity父布局中添加如下代码
原理实现
相信作为安卓开发工程师,你也接触过自定义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 + paddingBottomyPro = 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