热门标签 | HotTags
当前位置:  开发笔记 > Android > 正文

教你五分钟实现Android超漂亮的刻度轮播控件实例教程

说到轮播图,想必大家都不陌生,下面这篇文章主要给大家介绍了关于如何利用五分钟快速实现一款超漂亮的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)  {//计算手指拖动距离,并记录新的坐标重绘界面
    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&#63;)
   : super(context, attrs) {
  resolveAttribute(context, attrs, 0, 0)
  init()
 }

 constructor(context: Context, attrs: AttributeSet&#63;, @AttrRes defStyleAttr: Int)
   : super(context, attrs, defStyleAttr) {
  resolveAttribute(context, attrs, defStyleAttr, 0)
  init()
 }

 /**
  * 从xml属性初始化参数
  */
 private fun resolveAttribute(context: Context, attrs: AttributeSet&#63;, 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  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 = 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)  {//计算手指拖动距离,并记录新的坐标重绘界面
     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()
    (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())
  }
 }
}

&#8195;&#8195;attrs.xml

<&#63;xml version="1.0" encoding="utf-8"&#63;>

 
  
  
  
  
  
 

&#8195;&#8195;sample

欢迎大家关注一下我开源的一个音视频库,HardwareVideoCodec是一个高效的Android音视频编码库,支持软编和硬编。使用它你可以很容易的实现任何分辨率的视频编码,无需关心摄像头预览大小。一切都如此简单。目前已迭代多个稳定版本,欢迎查阅学习和使用,如有BUG或建议,欢迎Issue。

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对的支持。


推荐阅读
  • 本文介绍了使用kotlin实现动画效果的方法,包括上下移动、放大缩小、旋转等功能。通过代码示例演示了如何使用ObjectAnimator和AnimatorSet来实现动画效果,并提供了实现抖动效果的代码。同时还介绍了如何使用translationY和translationX来实现上下和左右移动的效果。最后还提供了一个anim_small.xml文件的代码示例,可以用来实现放大缩小的效果。 ... [详细]
  • Android中高级面试必知必会,积累总结
    本文介绍了Android中高级面试的必知必会内容,并总结了相关经验。文章指出,如今的Android市场对开发人员的要求更高,需要更专业的人才。同时,文章还给出了针对Android岗位的职责和要求,并提供了简历突出的建议。 ... [详细]
  • android listview OnItemClickListener失效原因
    最近在做listview时发现OnItemClickListener失效的问题,经过查找发现是因为button的原因。不仅listitem中存在button会影响OnItemClickListener事件的失效,还会导致单击后listview每个item的背景改变,使得item中的所有有关焦点的事件都失效。本文给出了一个范例来说明这种情况,并提供了解决方法。 ... [详细]
  • 本文讨论了Alink回归预测的不完善问题,指出目前主要针对Python做案例,对其他语言支持不足。同时介绍了pom.xml文件的基本结构和使用方法,以及Maven的相关知识。最后,对Alink回归预测的未来发展提出了期待。 ... [详细]
  • 本文讲述了如何通过代码在Android中更改Recycler视图项的背景颜色。通过在onBindViewHolder方法中设置条件判断,可以实现根据条件改变背景颜色的效果。同时,还介绍了如何修改底部边框颜色以及提供了RecyclerView Fragment layout.xml和项目布局文件的示例代码。 ... [详细]
  • 本文介绍了OC学习笔记中的@property和@synthesize,包括属性的定义和合成的使用方法。通过示例代码详细讲解了@property和@synthesize的作用和用法。 ... [详细]
  • 在说Hibernate映射前,我们先来了解下对象关系映射ORM。ORM的实现思想就是将关系数据库中表的数据映射成对象,以对象的形式展现。这样开发人员就可以把对数据库的操作转化为对 ... [详细]
  • 本文介绍了在SpringBoot中集成thymeleaf前端模版的配置步骤,包括在application.properties配置文件中添加thymeleaf的配置信息,引入thymeleaf的jar包,以及创建PageController并添加index方法。 ... [详细]
  • XML介绍与使用的概述及标签规则
    本文介绍了XML的基本概念和用途,包括XML的可扩展性和标签的自定义特性。同时还详细解释了XML标签的规则,包括标签的尖括号和合法标识符的组成,标签必须成对出现的原则以及特殊标签的使用方法。通过本文的阅读,读者可以对XML的基本知识有一个全面的了解。 ... [详细]
  • 拥抱Android Design Support Library新变化(导航视图、悬浮ActionBar)
    转载请注明明桑AndroidAndroid5.0Loollipop作为Android最重要的版本之一,为我们带来了全新的界面风格和设计语言。看起来很受欢迎࿰ ... [详细]
  • Java验证码——kaptcha的使用配置及样式
    本文介绍了如何使用kaptcha库来实现Java验证码的配置和样式设置,包括pom.xml的依赖配置和web.xml中servlet的配置。 ... [详细]
  • Android系统移植与调试之如何修改Android设备状态条上音量加减键在横竖屏切换的时候的显示于隐藏
    本文介绍了如何修改Android设备状态条上音量加减键在横竖屏切换时的显示与隐藏。通过修改系统文件system_bar.xml实现了该功能,并分享了解决思路和经验。 ... [详细]
  • 本文介绍了Android 7的学习笔记总结,包括最新的移动架构视频、大厂安卓面试真题和项目实战源码讲义。同时还分享了开源的完整内容,并提醒读者在使用FileProvider适配时要注意不同模块的AndroidManfiest.xml中配置的xml文件名必须不同,否则会出现问题。 ... [详细]
  • Android源码深入理解JNI技术的概述和应用
    本文介绍了Android源码中的JNI技术,包括概述和应用。JNI是Java Native Interface的缩写,是一种技术,可以实现Java程序调用Native语言写的函数,以及Native程序调用Java层的函数。在Android平台上,JNI充当了连接Java世界和Native世界的桥梁。本文通过分析Android源码中的相关文件和位置,深入探讨了JNI技术在Android开发中的重要性和应用场景。 ... [详细]
  • Spring源码解密之默认标签的解析方式分析
    本文分析了Spring源码解密中默认标签的解析方式。通过对命名空间的判断,区分默认命名空间和自定义命名空间,并采用不同的解析方式。其中,bean标签的解析最为复杂和重要。 ... [详细]
author-avatar
灰包蛋啦_199
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有