教你五分鐘實現(xiàn)Android超漂亮的刻度輪播控件實例教程
前言
最近一直在做音視頻的工作,已經(jīng)有大半年沒有寫應(yīng)用層的東西了,生怕越來越生疏。正好前段時間接了個外包項目,才得以回顧一下。項目中有一個控件挺簡潔漂亮的,而且用到的技術(shù)也比較基礎(chǔ),比較適合新手學(xué)習(xí),所以單獨開源出來,希望能對初學(xué)者有所幫助。

截圖

截屏
一、自定義View的常用方法
相信每個Android程序員都知道,我們每天的開發(fā)工作當(dāng)中都在不停地跟View打交道,Android中的任何一個布局、任何一個控件其實都是直接或間接繼承自View的,如TextView、Button、ImageView、ListView等。
一些接觸Android不久的朋友對自定義View都有一絲畏懼感,總感覺這是一個比較高級的技術(shù),但其實自定義View并不復(fù)雜,有時候只需要簡單幾行代碼就可以完成了。
說到自定義View,總繞不開下面幾個方法
1. override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int)
初始化View時,用于測量大小,并對View的大小進行控制,比如可以控制View的寬高比例。
2. override fun onDraw(canvas: Canvas)
View的繪制回調(diào),所有的畫筆、畫布操作都在這里。切勿在此方法進行耗時操作,能在外部計算的都在外部計算,并且盡量不要在這里初始化變量。因為正常情況下這個方法會以60fps的速度進行回調(diào),如果有耗時操作,將會卡頓,如果初始化大量對象,則會消耗大量內(nèi)存??傊?,跟畫布無關(guān)的操作都不要寫在這里。
3. invalidate()
用于通知View進行重繪,也就是重新調(diào)用onDraw,當(dāng)我們界面屬性發(fā)生變化時,就可以調(diào)用該方法來進行重繪,而不是調(diào)用onDraw,這個方法非常常用。
4. override fun onTouchEvent(event: MotionEvent): Boolean
相信大家都知道,這個是觸摸事件回調(diào)。在這里可以處理一些手勢操作。
二、自定義一個刻度控件RulerView
由于代碼比較多,而且源碼里面的注釋也比較詳細,所以這里只挑重點的幾個方法講解一下。如果有問題,或者錯誤,歡迎在評論區(qū)留言。
觀察本文開始的視頻,我們可以發(fā)現(xiàn),該控件雖然看起來挺簡潔,但是需要控制的部分卻不少,光刻度就有三種類型,還有一些文字。
普通刻度,寬度比較短,顏色比較淺,不帶文字。
整10刻度,寬度比較長,顏色相較普通刻度深一點,并且?guī)в形淖帧?br /> 游標(biāo)刻度,寬度在三類刻度里面是最長的,顏色高亮,并且也帶有文字。
標(biāo)簽文字,用于描述該刻度的用途。
以上都是需要我們用畫筆來繪制的,所以我們定義了以下幾個畫筆,為了避免在onDraw中頻繁更改畫筆屬性,這里又對文字和刻度定義了單獨的畫筆,目的是避免任何畫筆屬性的改變和在onDraw中改變屬性導(dǎo)致繪制過于耗時,更重要的是來回更改畫筆的屬性過于復(fù)雜,不便于操作和問題排查。
scalePaint: Paint //刻度畫筆
scalePointerPaint: Paint //整10刻度文字畫筆
scalePointerTextPaint: Paint //整10刻度文字畫筆
cursorPaint: Paint //游標(biāo)畫筆
cursorTextPaint: Paint //游標(biāo)文字畫筆
cursorLabelPaint: Paint //標(biāo)簽文字畫筆
1、從xml設(shè)置的屬性初始化參數(shù)
除了基礎(chǔ)的畫筆對象,還需要一些畫筆必要的屬性,比如我們繪制一個刻度,需要知道刻度位置、大小和間距。所以圍繞這些,又定義了一系列屬性。這些屬性可以由xml定義時提供,由此引出View的另一個重要用法。
這個用法比較固定,都是這個套路。其中需要注意的是,類似于R.styleable.app_scaleWidth這種id是在values/attrs.xml中定義的,app代表命名空間,可以自定義,scaleWidth就是屬性id,跟layout_width這些是一樣的。我們在一個命名空間中定義了一個屬性id后,就可以像使用layout_width和layout_height那樣從xml中向View傳遞屬性了。此時在View的構(gòu)造方法中可以直接獲取這些屬性值,代碼如下。
/**
* 從xml設(shè)置的屬性初始化參數(shù)
*/
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屬性來記錄滾動位置,通過這個屬性繪制相應(yīng)的位置,來實現(xiàn)滾動效果。這樣操作可以通過指定繪制區(qū)域(屏幕外的內(nèi)容不繪制,感興趣的同學(xué)可以去嘗試實現(xiàn))來解決性能問題。
drawScale通過遍歷items來繪制每一個元素,包括刻度和對應(yīng)的文字,都是比較基本的操作。需要注意的是canvas.drawText默認(rèn)情況下的x,y是指文字的左下角位置。
private fun drawScale(canvas: Canvas) {
for (i in 0 until items.size) {//根據(jù)給定的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)
}
}
}
/**
* 繪制游標(biāo),這里也需要計算文字位置,包括item文字和標(biāo)簽文字
*/
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的手勢滾動操作比較簡單,不需要自己去實現(xiàn)各種邏輯控制,而是通過系統(tǒng)提供的Scroller來計算滾動位置。
首先我們需要一個GestureDetectorCompat和OverScroller,前者用于手勢監(jiān)聽,后者通過MotionEvent來計算滾動位置。
1.mGestureDetector: GestureDetectorCompat
2.scroller: OverScroller
private fun init() {
mGestureDetector = GestureDetectorCompat(context, onGestureListener)
scroller = OverScroller(context)
}
構(gòu)造一個GestureDetectorCompat對象,需要先提供一個OnGestureListener,用來監(jiān)聽onScroll和onFling事件。其實就是MotionEvent經(jīng)過GestureDetectorCompat處理之后,就變成了可以直接使用的滾動和慣性滾動事件,然后通過這兩個回調(diào)通知我們。
在onScroll中,我們通過橫向和縱向滾動距離來計算滾動方向,如果橫向滾動距離大于縱向滾動距離,我們則可以認(rèn)為是橫向滾動,反之則是縱向滾動。本文只需要縱向滾動。
拿到滾動方向之后,我們就可以對滾動位置x,y進行累加,記錄每一次滑動之后的新的位置。最后通過postInvalidateOnAnimation或invalidate來通知重新繪制,onDraw根據(jù)新的x,y繪制對應(yīng)位置的畫面,來實現(xiàn)滑動。
雖然通過onScroll已經(jīng)實現(xiàn)了View的滑動,但只是實現(xiàn)跟隨手指運動,還沒有實現(xiàn)“拋”的動作。在現(xiàn)實世界中,運動是有慣性的,如果只實現(xiàn)onScroll,一切都顯得很生硬。那么如何實現(xiàn)慣性運動呢,我們自己計算?想想都可怕,這么多運動函數(shù),相信不是一般人能應(yīng)付的來的。幸運的是,這個計算我們可以交給GestureDetectorCompat的onFling。
onFling有四個參數(shù),前兩個是MotionEvent,分別代表前后兩個觸摸事件。velocityX: Float代表X軸滾動速率,velocityY: Float代表Y軸滾動速率,我們不需要關(guān)心這兩個值如何,直接交給scroller處理即可。
這里也許有人要問了,我們的手指離開屏幕之后便不再產(chǎn)生事件,View是如何實現(xiàn)持續(xù)滑動的呢。再回頭看一下onFling回調(diào)也確實如此,onFling只會根據(jù)手指離開屏幕前兩個MotionEvent來計算速率,之后就再也沒有回調(diào),所以scroller.fling也僅僅是調(diào)用了一次,并不能持續(xù)滾動。那我們?nèi)绾螌崿F(xiàn)持續(xù)的慣性滾動呢?
要實現(xiàn)持續(xù)的慣性滾動,就得依賴于override fun computeScroll(),該方法由draw過程中調(diào)用,我們可以通過invalidate->onDraw->computeScroll->invalidate這樣一個循環(huán)來控制慣性滾動,直至慣性滾動停止,具體實現(xiàn)可以參考文章最后的源碼。
/**
* 手勢監(jiān)聽
*/
private val onGestureListener = object : GestureDetector.SimpleOnGestureListener() {
/**
* 手指按下回調(diào),這里將狀態(tài)標(biāo)記為非滾動狀態(tài)
*/
override fun onDown(e: MotionEvent): Boolean {
parent.requestDisallowInterceptTouchEvent(true)
mCurrentScrollDirection = Direction.NONE
return true
}
/**
* 手指拖動回調(diào)
*/
override fun onScroll(e1: MotionEvent, e2: MotionEvent, distanceX: Float, distanceY: Float): Boolean {
//如果當(dāng)前正在滾動,則停止?jié)L動
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 -> {//計算手指拖動距離,并記錄新的坐標(biāo)重繪界面
mCurrentOrigin.y -= distanceY
checkOriginY()
ViewCompat.postInvalidateOnAnimation(this@RulerView)
}
}
return true
}
/**
* 慣性滾動回調(diào)
*/
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的繪制和事件兩個重要部分都講完了。喜歡的話記得點贊、評論和關(guān)注,您的關(guān)注是我的鼓勵。文章最后貼出相關(guān)源碼,歡迎查閱學(xué)習(xí)。如果有問題,或者錯誤,歡迎在評論區(qū)留言。
完整代碼
RulerView.kt
class RulerView : View {
private enum class Direction {
NONE, VERTICAL
}
private var label: String = "LABEL"
private var items: List<*> = ItemCreator.range(0, 60)
//游標(biāo)顏色
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
//游標(biāo)寬度
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
//游標(biāo)畫筆
private lateinit var cursorPaint: Paint
//游標(biāo)文字畫筆
private lateinit var cursorTextPaint: Paint
//標(biāo)簽文字畫筆
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
//當(dāng)前滾動方向
private var mCurrentScrollDirection = Direction.NONE
//當(dāng)前慣性滾動方向
private var mCurrentFlingDirection = Direction.NONE
//當(dāng)前滾動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屬性初始化參數(shù)
*/
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()
}
/**
* 設(shè)置item數(shù)據(jù)
*/
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數(shù)據(jù)
*/
fun getItems(): List<*> {
return items
}
/**
* 設(shè)置標(biāo)簽文字
*/
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()//檢查是否需要對齊刻度,如果需要,則自動滾動,讓游標(biāo)與刻度對齊
}
} else {
//如果當(dāng)前不處于滾動狀態(tài),則再次檢查是否需要對齊刻度
if (mCurrentFlingDirection != Direction.NONE && forceFinishScroll()) {
snapScroll()
} else if (scroller.computeScrollOffset()) {//檢查是否滾動完成,并且計算新的滾動坐標(biāo)
mCurrentOrigin.y = scroller.currY.toFloat()//記錄當(dāng)前y坐標(biāo)
checkOriginY()//檢查坐標(biāo)是否越界
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)
//繪制游標(biāo)
drawCursor(canvas)
}
private fun drawScale(canvas: Canvas) {
for (i in 0 until items.size) {//根據(jù)給定的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)
}
}
}
/**
* 繪制游標(biāo),這里也需要計算文字位置,包括item文字和標(biāo)簽文字
*/
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坐標(biāo)越界
*/
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
}
/**
* 設(shè)置選中item
*/
fun setSelectedItem(index: Int) {
post {
mCurrentOrigin.y = -(scaleHeight * index).toFloat()
checkOriginY()
ViewCompat.postInvalidateOnAnimation(this@RulerView)
snapScroll()
}
}
/**
* 手勢監(jiān)聽
*/
private val onGestureListener = object : GestureDetector.SimpleOnGestureListener() {
/**
* 手指按下回調(diào),這里將狀態(tài)標(biāo)記為非滾動狀態(tài)
*/
override fun onDown(e: MotionEvent): Boolean {
parent.requestDisallowInterceptTouchEvent(true)
mCurrentScrollDirection = Direction.NONE
return true
}
/**
* 手指拖動回調(diào)
*/
override fun onScroll(e1: MotionEvent, e2: MotionEvent, distanceX: Float, distanceY: Float): Boolean {
//如果當(dāng)前正在滾動,則停止?jié)L動
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 -> {//計算手指拖動距離,并記錄新的坐標(biāo)重繪界面
mCurrentOrigin.y -= distanceY
checkOriginY()
ViewCompat.postInvalidateOnAnimation(this@RulerView)
}
}
return true
}
/**
* 慣性滾動回調(diào)
*/
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" />
歡迎大家關(guān)注一下我開源的一個音視頻庫,HardwareVideoCodec是一個高效的Android音視頻編碼庫,支持軟編和硬編。使用它你可以很容易的實現(xiàn)任何分辨率的視頻編碼,無需關(guān)心攝像頭預(yù)覽大小。一切都如此簡單。目前已迭代多個穩(wěn)定版本,歡迎查閱學(xué)習(xí)和使用,如有BUG或建議,歡迎Issue。
總結(jié)
以上就是這篇文章的全部內(nèi)容了,希望本文的內(nèi)容對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,如果有疑問大家可以留言交流,謝謝大家對腳本之家的支持。
- Android實現(xiàn)圖片輪播效果的兩種方法
- Android實現(xiàn)圖片輪播效果
- Android實現(xiàn)Banner界面廣告圖片循環(huán)輪播(包括實現(xiàn)手動滑動循環(huán))
- Android 使用ViewPager自動滾動循環(huán)輪播效果
- Android實現(xiàn)圖片自動輪播并且支持手勢左右無限滑動
- Android使用ViewPager加載圖片和輪播視頻
- Android實現(xiàn)圖片文字輪播特效
- Android自動播放Banner圖片輪播效果
- Android實現(xiàn)廣告圖片輪播效果
- Android ViewPager實現(xiàn)圖片輪播效果
相關(guān)文章
Android WebView交互傳遞json字符串并解析的方法
這篇文章主要給大家介紹了關(guān)于Android中WebView交互傳遞json字符串并解析的相關(guān)資料,文中通過示例代碼介紹的非常詳細,對各位Android開發(fā)者具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧。2018-05-05
Android編程獲取GPS數(shù)據(jù)的方法詳解
這篇文章主要介紹了Android編程獲取GPS數(shù)據(jù)的方法,結(jié)合實例形式分析了Android地理位置操作的相關(guān)函數(shù)與使用技巧,需要的朋友可以參考下2016-10-10
Android ProgressBar實現(xiàn)進度條效果
這篇文章主要為大家詳細介紹了Android ProgressBar實現(xiàn)進度條效果,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2022-04-04
android調(diào)用web service(cxf)實例應(yīng)用詳解
Google為ndroid平臺開發(fā)Web Service提供了支持,提供了Ksoap2-android相關(guān)架包接下來介紹android調(diào)用web service(cxf),感興趣的朋友可以了解下2013-01-01

