教你五分钟实现Android超漂亮的刻度轮播控件实例教程
前言
最近一直在做音视频的工作,已经有大半年没有写应用层的东西了,生怕越来越生疏。正好前段时间接了个外包项目,才得以回顾一下。项目中有一个控件挺简洁漂亮的,而且用到的技术也比较基础,比较适合新手学习,所以单独开源出来,希望能对初学者有所帮助。
截图
截屏
一、自定义View的常用方法
相信每个Android程序员都知道,我们每天的开发工作当中都在不停地跟View打交道,Android中的任何一个布局、任何一个控件其实都是直接或间接继承自View的,如TextView、Button、ImageView、ListView等。
一些接触Android不久的朋友对自定义View都有一丝畏惧感,总感觉这是一个比较高级的技术,但其实自定义View并不复杂,有时候只需要简单几行代码就可以完成了。
说到自定义View,总绕不开下面几个方法
1. override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int)
初始化View时,用于测量大小,并对View的大小进行控制,比如可以控制View的宽高比例。
2. override fun onDraw(canvas: Canvas)
View的绘制回调,所有的画笔、画布操作都在这里。切勿在此方法进行耗时操作,能在外部计算的都在外部计算,并且尽量不要在这里初始化变量。因为正常情况下这个方法会以60fps的速度进行回调,如果有耗时操作,将会卡顿,如果初始化大量对象,则会消耗大量内存。总之,跟画布无关的操作都不要写在这里。
3. invalidate()
用于通知View进行重绘,也就是重新调用onDraw,当我们界面属性发生变化时,就可以调用该方法来进行重绘,而不是调用onDraw,这个方法非常常用。
4. override fun onTouchEvent(event: MotionEvent): Boolean
相信大家都知道,这个是触摸事件回调。在这里可以处理一些手势操作。
二、自定义一个刻度控件RulerView
由于代码比较多,而且源码里面的注释也比较详细,所以这里只挑重点的几个方法讲解一下。如果有问题,或者错误,欢迎在评论区留言。
观察本文开始的视频,我们可以发现,该控件虽然看起来挺简洁,但是需要控制的部分却不少,光刻度就有三种类型,还有一些文字。
普通刻度,宽度比较短,颜色比较浅,不带文字。
整10刻度,宽度比较长,颜色相较普通刻度深一点,并且带有文字。
游标刻度,宽度在三类刻度里面是最长的,颜色高亮,并且也带有文字。
标签文字,用于描述该刻度的用途。
以上都是需要我们用画笔来绘制的,所以我们定义了以下几个画笔,为了避免在onDraw中频繁更改画笔属性,这里又对文字和刻度定义了单独的画笔,目的是避免任何画笔属性的改变和在onDraw中改变属性导致绘制过于耗时,更重要的是来回更改画笔的属性过于复杂,不便于操作和问题排查。
scalePaint: Paint //刻度画笔
scalePointerPaint: Paint //整10刻度文字画笔
scalePointerTextPaint: Paint //整10刻度文字画笔
cursorPaint: Paint //游标画笔
cursorTextPaint: Paint //游标文字画笔
cursorLabelPaint: Paint //标签文字画笔
1、从xml设置的属性初始化参数
除了基础的画笔对象,还需要一些画笔必要的属性,比如我们绘制一个刻度,需要知道刻度位置、大小和间距。所以围绕这些,又定义了一系列属性。这些属性可以由xml定义时提供,由此引出View的另一个重要用法。
这个用法比较固定,都是这个套路。其中需要注意的是,类似于R.styleable.app_scaleWidth这种id是在values/attrs.xml中定义的,app代表命名空间,可以自定义,scaleWidth就是属性id,跟layout_width这些是一样的。我们在一个命名空间中定义了一个属性id后,就可以像使用layout_width和layout_height那样从xml中向View传递属性了。此时在View的构造方法中可以直接获取这些属性值,代码如下。
/** * 从xml设置的属性初始化参数 */ private fun resolveAttribute(context: Context, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int) { scaleStrokeWidth = dpToPx(context, 1f) / 2f scaleWidth = 50 scalePointerWidth = (scaleWidth * 1.5).toInt() cursorWidth = (scaleWidth * 3.333).toInt() scaleHeight = 5 cursorColor = context.resources.getColor(R.color.red) scaleColor = context.resources.getColor(R.color.grey_888) scalePointerColor = context.resources.getColor(R.color.grey_800) val a = context.theme.obtainStyledAttributes(attrs, R.styleable.app, defStyleAttr, defStyleRes) for (i in 0 until a.indexCount) { val attr = a.getIndex(i) if (attr == R.styleable.app_scaleWidth) { scaleWidth = a.getDimensionPixelOffset(attr, 50) scalePointerWidth = (scaleWidth * 1.5).toInt() cursorWidth = (scaleWidth * 3.333).toInt() } else if (attr == R.styleable.app_scaleHeight) { scaleHeight = a.getDimensionPixelOffset(attr, 5) } else if (attr == R.styleable.app_cursorColor) { cursorColor = a.getColor(attr, context.resources.getColor(R.color.red)) } else if (attr == R.styleable.app_scaleColor) { scaleColor = a.getColor(attr, context.resources.getColor(R.color.grey_888)) } else if (attr == R.styleable.app_scalePointerColor) { scalePointerColor = a.getColor(attr, context.resources.getColor(R.color.grey_800)) } } cursorTextOffsetLeft = dpToPx(context, 32f) a.recycle() }
2、绘制View
本文并没有使用View提供的scrollTo和scrollBy来控制滚动,而是重新定义一个x,y属性来记录滚动位置,通过这个属性绘制相应的位置,来实现滚动效果。这样操作可以通过指定绘制区域(屏幕外的内容不绘制,感兴趣的同学可以去尝试实现)来解决性能问题。
drawScale通过遍历items来绘制每一个元素,包括刻度和对应的文字,都是比较基本的操作。需要注意的是canvas.drawText默认情况下的x,y是指文字的左下角位置。
private fun drawScale(canvas: Canvas) { for (i in 0 until items.size) {//根据给定的item信息绘制刻度 val top = offsetHeight + mCurrentOrigin.y.toInt() + i * scaleHeight + scaleHeight / 2 if (0 == i % 10) {//绘制整10刻度 canvas.drawRect(RectF(pointerScaleLeft.toFloat(), top - scaleStrokeWidth, pointerScaleLeft.toFloat() + scalePointerWidth, top + scaleStrokeWidth), scalePointerPaint) if (Math.abs(getSelectedItem() - i) > 1) {//整10刻度有文字,所以需要计算文字位置,并绘制文字 val text = items[i].toString() val size = measureTextSize(scalePointerTextPaint, text) canvas.drawText(text, pointerScaleLeft - size[0] * 1.3f, top + size[1] / 2, scalePointerTextPaint) } } else {//绘制普通刻度 canvas.drawRect(RectF(scaleLeft.toFloat(), top - scaleStrokeWidth, scaleLeft.toFloat() + scaleWidth, top + scaleStrokeWidth), scalePaint) } } } /** * 绘制游标,这里也需要计算文字位置,包括item文字和标签文字 */ private fun drawCursor(canvas: Canvas) { val left = scaleLeft + scaleWidth - cursorWidth val top = measuredHeight / 2f canvas.drawRect(RectF(left.toFloat(), top.toInt() - scaleStrokeWidth, left.toFloat() + cursorWidth, top.toInt() + scaleStrokeWidth), cursorPaint) val text = items[getSelectedItem()].toString() val textSize = measureTextSize(cursorTextPaint, text) val labelSize = measureTextSize(cursorLabelPaint, label) val labelLeft = left - cursorTextOffsetLeft - labelSize[0] val textOffset = (textSize[0] - labelSize[0]) / 2f canvas.drawText(text, left - cursorTextOffsetLeft - textSize[0] + textOffset, top + textSize[1] / 2, cursorTextPaint) canvas.drawText(label, labelLeft, top + textSize[1] + labelSize[1], cursorLabelPaint) }
3、支持滚动
Android的手势滚动操作比较简单,不需要自己去实现各种逻辑控制,而是通过系统提供的Scroller来计算滚动位置。
首先我们需要一个GestureDetectorCompat和OverScroller,前者用于手势监听,后者通过MotionEvent来计算滚动位置。
1.mGestureDetector: GestureDetectorCompat
2.scroller: OverScroller
private fun init() { mGestureDetector = GestureDetectorCompat(context, onGestureListener) scroller = OverScroller(context) }
构造一个GestureDetectorCompat对象,需要先提供一个OnGestureListener,用来监听onScroll和onFling事件。其实就是MotionEvent经过GestureDetectorCompat处理之后,就变成了可以直接使用的滚动和惯性滚动事件,然后通过这两个回调通知我们。
在onScroll中,我们通过横向和纵向滚动距离来计算滚动方向,如果横向滚动距离大于纵向滚动距离,我们则可以认为是横向滚动,反之则是纵向滚动。本文只需要纵向滚动。
拿到滚动方向之后,我们就可以对滚动位置x,y进行累加,记录每一次滑动之后的新的位置。最后通过postInvalidateOnAnimation或invalidate来通知重新绘制,onDraw根据新的x,y绘制对应位置的画面,来实现滑动。
虽然通过onScroll已经实现了View的滑动,但只是实现跟随手指运动,还没有实现“抛”的动作。在现实世界中,运动是有惯性的,如果只实现onScroll,一切都显得很生硬。那么如何实现惯性运动呢,我们自己计算?想想都可怕,这么多运动函数,相信不是一般人能应付的来的。幸运的是,这个计算我们可以交给GestureDetectorCompat的onFling。
onFling有四个参数,前两个是MotionEvent,分别代表前后两个触摸事件。velocityX: Float代表X轴滚动速率,velocityY: Float代表Y轴滚动速率,我们不需要关心这两个值如何,直接交给scroller处理即可。
这里也许有人要问了,我们的手指离开屏幕之后便不再产生事件,View是如何实现持续滑动的呢。再回头看一下onFling回调也确实如此,onFling只会根据手指离开屏幕前两个MotionEvent来计算速率,之后就再也没有回调,所以scroller.fling也仅仅是调用了一次,并不能持续滚动。那我们如何实现持续的惯性滚动呢?
要实现持续的惯性滚动,就得依赖于override fun computeScroll(),该方法由draw过程中调用,我们可以通过invalidate->onDraw->computeScroll->invalidate这样一个循环来控制惯性滚动,直至惯性滚动停止,具体实现可以参考文章最后的源码。
/** * 手势监听 */ private val onGestureListener = object : GestureDetector.SimpleOnGestureListener() { /** * 手指按下回调,这里将状态标记为非滚动状态 */ override fun onDown(e: MotionEvent): Boolean { parent.requestDisallowInterceptTouchEvent(true) mCurrentScrollDirection = Direction.NONE return true } /** * 手指拖动回调 */ override fun onScroll(e1: MotionEvent, e2: MotionEvent, distanceX: Float, distanceY: Float): Boolean { //如果当前正在滚动,则停止滚动 scroller.forceFinished(true) // Log.e(RulerView::class.java, "onScroll: ${mCurrentOrigin.y}, $distanceY") if (Direction.NONE == mCurrentScrollDirection) {//判断滚动方向,这里只有垂直一个方向 mCurrentScrollDirection = if (Math.abs(distanceX) < Math.abs(distanceY)) { Direction.VERTICAL } else { Direction.NONE } } // Calculate the new origin after scroll. when (mCurrentScrollDirection) { Direction.VERTICAL -> {//计算手指拖动距离,并记录新的坐标重绘界面 mCurrentOrigin.y -= distanceY checkOriginY() ViewCompat.postInvalidateOnAnimation(this@RulerView) } } return true } /** * 惯性滚动回调 */ override fun onFling(e1: MotionEvent, e2: MotionEvent, velocityX: Float, velocityY: Float): Boolean { scroller.forceFinished(true) mCurrentFlingDirection = mCurrentScrollDirection when (mCurrentFlingDirection) { Direction.VERTICAL -> scroller.fling(mCurrentOrigin.x.toInt(), mCurrentOrigin.y.toInt(), 0, velocityY.toInt(), Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE, 0) } ViewCompat.postInvalidateOnAnimation(this@RulerView) return true } }
至此自定义View的绘制和事件两个重要部分都讲完了。喜欢的话记得点赞、评论和关注,您的关注是我的鼓励。文章最后贴出相关源码,欢迎查阅学习。如果有问题,或者错误,欢迎在评论区留言。
完整代码
RulerView.kt
class RulerView : View { private enum class Direction { NONE, VERTICAL } private var label: String = "LABEL" private var items: List<*> = ItemCreator.range(0, 60) //游标颜色 private var cursorColor = 0 //刻度颜色 private var scaleColor = 0 //整10刻度颜色 private var scalePointerColor = 0 //可滚动高度 private var scrollHeight = 0f //刻度宽度 private var scaleWidth = 0 //整10刻度宽度 private var scalePointerWidth = 0 //游标宽度 private var cursorWidth = 0 //刻度高度+刻度间距 private var scaleHeight = 0 //刻度高度 private var scaleStrokeWidth = 0f //刻度画笔 private lateinit var scalePaint: Paint //整10刻度画笔 private lateinit var scalePointerPaint: Paint //整10刻度文字画笔 private lateinit var scalePointerTextPaint: Paint //游标画笔 private lateinit var cursorPaint: Paint //游标文字画笔 private lateinit var cursorTextPaint: Paint //标签文字画笔 private lateinit var cursorLabelPaint: Paint //刻度间距 private var offsetHeight = 0 //刻度与文字的间距 private var cursorTextOffsetLeft = 0 //刻度距离View左边的距离 private var scaleLeft = 0 //整10刻度距离View左边的距离 private var pointerScaleLeft = 0 //滚动控制器 private lateinit var scroller: OverScroller private var maxFlingVelocity = 0 private var minFlingVelocity = 0 private var touchSlop = 0 //当前滚动方向 private var mCurrentScrollDirection = Direction.NONE //当前惯性滚动方向 private var mCurrentFlingDirection = Direction.NONE //当前滚动x,y private val mCurrentOrigin = PointF(0f, 0f) //手势支持 private lateinit var mGestureDetector: GestureDetectorCompat constructor(context: Context) : super(context) { resolveAttribute(context, null, 0, 0) init() } constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) { resolveAttribute(context, attrs, 0, 0) init() } constructor(context: Context, attrs: AttributeSet?, @AttrRes defStyleAttr: Int) : super(context, attrs, defStyleAttr) { resolveAttribute(context, attrs, defStyleAttr, 0) init() } /** * 从xml属性初始化参数 */ private fun resolveAttribute(context: Context, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int) { scaleStrokeWidth = dpToPx(context, 1f) / 2f scaleWidth = 50 scalePointerWidth = (scaleWidth * 1.5).toInt() cursorWidth = (scaleWidth * 3.333).toInt() scaleHeight = 5 cursorColor = context.resources.getColor(R.color.red) scaleColor = context.resources.getColor(R.color.grey_888) scalePointerColor = context.resources.getColor(R.color.grey_800) val a = context.theme.obtainStyledAttributes(attrs, R.styleable.app, defStyleAttr, defStyleRes) for (i in 0 until a.indexCount) { val attr = a.getIndex(i) if (attr == R.styleable.app_scaleWidth) { scaleWidth = a.getDimensionPixelOffset(attr, 50) scalePointerWidth = (scaleWidth * 1.5).toInt() cursorWidth = (scaleWidth * 3.333).toInt() } else if (attr == R.styleable.app_scaleHeight) { scaleHeight = a.getDimensionPixelOffset(attr, 5) } else if (attr == R.styleable.app_cursorColor) { cursorColor = a.getColor(attr, context.resources.getColor(R.color.red)) } else if (attr == R.styleable.app_scaleColor) { scaleColor = a.getColor(attr, context.resources.getColor(R.color.grey_888)) } else if (attr == R.styleable.app_scalePointerColor) { scalePointerColor = a.getColor(attr, context.resources.getColor(R.color.grey_800)) } } cursorTextOffsetLeft = dpToPx(context, 32f) a.recycle() } /** * 初始化画笔、滚动控制器和手势对象 */ private fun init() { scroller = OverScroller(context) mGestureDetector = GestureDetectorCompat(context, onGestureListener) maxFlingVelocity = ViewConfiguration.get(context).scaledMaximumFlingVelocity minFlingVelocity = ViewConfiguration.get(context).scaledMinimumFlingVelocity touchSlop = ViewConfiguration.get(context).scaledTouchSlop scalePaint = Paint(Paint.ANTI_ALIAS_FLAG) scalePaint.color = scaleColor scalePaint.style = Paint.Style.FILL scalePointerPaint = Paint(Paint.ANTI_ALIAS_FLAG) scalePointerPaint.color = scalePointerColor scalePointerPaint.style = Paint.Style.FILL scalePointerTextPaint = Paint(Paint.ANTI_ALIAS_FLAG) scalePointerTextPaint.color = scaleColor scalePointerTextPaint.style = Paint.Style.FILL scalePointerTextPaint.textSize = spToPx(context, 14f).toFloat() cursorPaint = Paint(Paint.ANTI_ALIAS_FLAG) cursorPaint.color = cursorColor cursorPaint.style = Paint.Style.FILL cursorTextPaint = Paint(Paint.ANTI_ALIAS_FLAG) cursorTextPaint.color = context.resources.getColor(R.color.black_232) cursorTextPaint.style = Paint.Style.FILL cursorTextPaint.textSize = spToPx(context, 32f).toFloat() cursorLabelPaint = Paint(Paint.ANTI_ALIAS_FLAG) cursorLabelPaint.color = scalePointerColor cursorLabelPaint.style = Paint.Style.FILL cursorLabelPaint.textSize = spToPx(context, 16f).toFloat() } /** * 设置item数据 */ fun setItems(items: List<*>) { this.items = items this.scrollHeight = (height + (this.items.size - 1) * scaleHeight).toFloat() post { mCurrentOrigin.x = 0f mCurrentOrigin.y = 0f invalidate() } } /** * 获取item数据 */ fun getItems(): List<*> { return items } /** * 设置标签文字 */ fun setLabel(label: String) { this.label = label //重新初始化刻度左距离 initScaleLeft() //通知重新绘制 invalidate() } /** * 触控事件交给mGestureDetector */ override fun onTouchEvent(event: MotionEvent): Boolean { val result = mGestureDetector.onTouchEvent(event) //如果手指离开屏幕,并且没有惯性滑动 if (event.action == MotionEvent.ACTION_UP && mCurrentFlingDirection == Direction.NONE) { if (mCurrentScrollDirection == Direction.VERTICAL) { //检查是否需要对齐刻度 snapScroll() } mCurrentScrollDirection = Direction.NONE } return result } /** * 计算View如何滑动 */ override fun computeScroll() { super.computeScroll() if (scroller.isFinished) {//滚动以及完成 if (mCurrentFlingDirection !== Direction.NONE) { // Snap to day after fling is finished. mCurrentFlingDirection = Direction.NONE snapScroll()//检查是否需要对齐刻度,如果需要,则自动滚动,让游标与刻度对齐 } } else { //如果当前不处于滚动状态,则再次检查是否需要对齐刻度 if (mCurrentFlingDirection != Direction.NONE && forceFinishScroll()) { snapScroll() } else if (scroller.computeScrollOffset()) {//检查是否滚动完成,并且计算新的滚动坐标 mCurrentOrigin.y = scroller.currY.toFloat()//记录当前y坐标 checkOriginY()//检查坐标是否越界 ViewCompat.postInvalidateOnAnimation(this)//通知重新绘制 } else {//不作滚动 val startY = if (mCurrentOrigin.y > 0) 0f else if (mCurrentOrigin.y < height - measuredHeight) measuredHeight - scrollHeight else mCurrentOrigin.y scroller.startScroll(0, startY.toInt(), 0, 0, 0) } } } /** * 测量控件大小,并初始化一些必要的属性 */ override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { super.onMeasure(widthMeasureSpec, heightMeasureSpec) val width = measuredWidth val height = measuredHeight offsetHeight = height / 2 - scaleHeight / 2 scrollHeight = height + (items.size - 1f) * scaleHeight initScaleLeft() pointerScaleLeft = scaleLeft + scaleWidth - scalePointerWidth } /** * 初始化刻度左间距 */ private fun initScaleLeft() { val labelSize = measureTextSize(cursorLabelPaint, label) scaleLeft = (measuredWidth - scalePointerWidth + cursorTextOffsetLeft + labelSize[0].toInt()) / 2 } override fun onDraw(canvas: Canvas) { super.onDraw(canvas) if (items.isEmpty()) return //绘制刻度 drawScale(canvas) //绘制游标 drawCursor(canvas) } private fun drawScale(canvas: Canvas) { for (i in 0 until items.size) {//根据给定的item信息绘制刻度 val top = offsetHeight + mCurrentOrigin.y.toInt() + i * scaleHeight + scaleHeight / 2 if (0 == i % 10) {//绘制整10刻度 canvas.drawRect(RectF(pointerScaleLeft.toFloat(), top - scaleStrokeWidth, pointerScaleLeft.toFloat() + scalePointerWidth, top + scaleStrokeWidth), scalePointerPaint) if (Math.abs(getSelectedItem() - i) > 1) {//整10刻度有文字,所以需要计算文字位置,并绘制文字 val text = items[i].toString() val size = measureTextSize(scalePointerTextPaint, text) canvas.drawText(text, pointerScaleLeft - size[0] * 1.3f, top + size[1] / 2, scalePointerTextPaint) } } else {//绘制普通刻度 canvas.drawRect(RectF(scaleLeft.toFloat(), top - scaleStrokeWidth, scaleLeft.toFloat() + scaleWidth, top + scaleStrokeWidth), scalePaint) } } } /** * 绘制游标,这里也需要计算文字位置,包括item文字和标签文字 */ private fun drawCursor(canvas: Canvas) { val left = scaleLeft + scaleWidth - cursorWidth val top = measuredHeight / 2f canvas.drawRect(RectF(left.toFloat(), top.toInt() - scaleStrokeWidth, left.toFloat() + cursorWidth, top.toInt() + scaleStrokeWidth), cursorPaint) val text = items[getSelectedItem()].toString() val textSize = measureTextSize(cursorTextPaint, text) val labelSize = measureTextSize(cursorLabelPaint, label) val labelLeft = left - cursorTextOffsetLeft - labelSize[0] val textOffset = (textSize[0] - labelSize[0]) / 2f canvas.drawText(text, left - cursorTextOffsetLeft - textSize[0] + textOffset, top + textSize[1] / 2, cursorTextPaint) canvas.drawText(label, labelLeft, top + textSize[1] + labelSize[1], cursorLabelPaint) } private fun forceFinishScroll(): Boolean { return scroller.currVelocity <= minFlingVelocity } /** * 与刻度对齐 */ private fun snapScroll() { scroller.computeScrollOffset() val nearestOrigin = -getSelectedItem() * scaleHeight mCurrentOrigin.y = nearestOrigin.toFloat() ViewCompat.postInvalidateOnAnimation(this@RulerView) } /** * 检查y坐标越界 */ private fun checkOriginY() { if (mCurrentOrigin.y > 0) mCurrentOrigin.y = 0f if (mCurrentOrigin.y < measuredHeight - scrollHeight) mCurrentOrigin.y = measuredHeight - scrollHeight } /** * 获取选中的item */ fun getSelectedItem(): Int { var index = -Math.round(mCurrentOrigin.y / scaleHeight) if (index >= items.size) index = items.size - 1 if (index < 0) index = 0 return index } /** * 设置选中item */ fun setSelectedItem(index: Int) { post { mCurrentOrigin.y = -(scaleHeight * index).toFloat() checkOriginY() ViewCompat.postInvalidateOnAnimation(this@RulerView) snapScroll() } } /** * 手势监听 */ private val onGestureListener = object : GestureDetector.SimpleOnGestureListener() { /** * 手指按下回调,这里将状态标记为非滚动状态 */ override fun onDown(e: MotionEvent): Boolean { parent.requestDisallowInterceptTouchEvent(true) mCurrentScrollDirection = Direction.NONE return true } /** * 手指拖动回调 */ override fun onScroll(e1: MotionEvent, e2: MotionEvent, distanceX: Float, distanceY: Float): Boolean { //如果当前正在滚动,则停止滚动 scroller.forceFinished(true) // Log.e(RulerView::class.java, "onScroll: ${mCurrentOrigin.y}, $distanceY") if (Direction.NONE == mCurrentScrollDirection) {//判断滚动方向,这里只有垂直一个方向 mCurrentScrollDirection = if (Math.abs(distanceX) < Math.abs(distanceY)) { Direction.VERTICAL } else { Direction.NONE } } // Calculate the new origin after scroll. when (mCurrentScrollDirection) { Direction.VERTICAL -> {//计算手指拖动距离,并记录新的坐标重绘界面 mCurrentOrigin.y -= distanceY checkOriginY() ViewCompat.postInvalidateOnAnimation(this@RulerView) } } return true } /** * 惯性滚动回调 */ override fun onFling(e1: MotionEvent, e2: MotionEvent, velocityX: Float, velocityY: Float): Boolean { scroller.forceFinished(true) mCurrentFlingDirection = mCurrentScrollDirection when (mCurrentFlingDirection) { Direction.VERTICAL -> scroller.fling(mCurrentOrigin.x.toInt(), mCurrentOrigin.y.toInt(), 0, velocityY.toInt(), Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE, 0) } ViewCompat.postInvalidateOnAnimation(this@RulerView) return true } } class ItemCreator { companion object { fun range(start: Int, end: Int): List<*> { val result = ArrayList<Int>() (start..end).forEach { result.add(it) } return result } } } companion object { fun dpToPx(context: Context, dp: Float): Int { return Math.round(context.resources.displayMetrics.density * dp) } fun spToPx(context: Context, sp: Float): Int { return (TypedValue.applyDimension(2, sp, context.resources.displayMetrics) + 0.5f).toInt() } /** * 测量文字宽高 */ fun measureTextSize(paint: Paint, text: String): FloatArray { if (TextUtils.isEmpty(text)) return floatArrayOf(0f, 0f) val width = paint.measureText(text, 0, text.length) val bounds = Rect() paint.getTextBounds(text, 0, text.length, bounds) return floatArrayOf(width, bounds.height().toFloat()) } } }
attrs.xml
<?xml version="1.0" encoding="utf-8"?> <resources> <declare-styleable name="app"> <attr name="scaleWidth" format="dimension" /> <attr name="scaleHeight" format="dimension" /> <attr name="cursorColor" format="color" /> <attr name="scaleColor" format="color" /> <attr name="scalePointerColor" format="color" /> </declare-styleable> </resources>
sample
<com.lava.demo.widget.RulerView android:id="@+id/timeRuler" android:layout_width="match_parent" android:layout_height="match_parent" android:background="#FAFAFA" app:cursorColor="#F48B2E" app:scaleColor="#C9C9C9" app:scaleHeight="8dp" app:scalePointerColor="#999999" app:scaleWidth="12dp" />
欢迎大家关注一下我开源的一个音视频库,HardwareVideoCodec是一个高效的Android音视频编码库,支持软编和硬编。使用它你可以很容易的实现任何分辨率的视频编码,无需关心摄像头预览大小。一切都如此简单。目前已迭代多个稳定版本,欢迎查阅学习和使用,如有BUG或建议,欢迎Issue。
总结
以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对亿速云的支持。