Android自定義View繪制貝塞爾曲線中小紅點(diǎn)的方法
前言
上一篇文章用扇形圖練習(xí)了一下安卓的多點(diǎn)觸控,實(shí)現(xiàn)了單指旋轉(zhuǎn)、二指放大、三指移動(dòng),四指以上同時(shí)按下進(jìn)行復(fù)位的功能。今天這篇文章用很多應(yīng)用常見的小紅點(diǎn),來練習(xí)一下貝塞爾曲線的使用。
需求
這里想法來自QQ的拖動(dòng)小紅點(diǎn)取消顯示聊天條數(shù)功能,不過好像是記憶里的了,現(xiàn)在看了下好像效果變了。總而言之,就是一個(gè)小圓點(diǎn),拖動(dòng)的時(shí)候變成水滴狀,超過一定范圍后觸發(fā)消失回調(diào),核心思想如下:
1、一個(gè)正方形view,中間是小紅點(diǎn),小紅點(diǎn)距離邊框有一定距離
2、拖動(dòng)小紅點(diǎn),小紅點(diǎn)會(huì)變形,并產(chǎn)生尾焰效果
3、釋放時(shí),如果在設(shè)定范圍外小紅點(diǎn)消失,范圍內(nèi)則恢復(fù)
效果圖
這里效果在距離小的時(shí)候,還是不錯(cuò)的,當(dāng)移動(dòng)范圍過大時(shí),雖然水滴狀的曲線還是連續(xù)的,但是變形嚴(yán)重了,不過這個(gè)功能并不需要拖動(dòng)太長距離把,只要限定好消失范圍,還是能滿足要求的。
代碼
import android.animation.ValueAnimator import android.content.Context import android.graphics.* import android.util.AttributeSet import android.view.MotionEvent import android.view.View import androidx.core.animation.addListener import kotlin.math.asin import kotlin.math.atan2 import kotlin.math.cos import kotlin.math.sin /** * 拖拽消失的小紅點(diǎn) * * @author silence * @date 2022-11-07 * */ class RedDomView @JvmOverloads constructor( context: Context, attributeSet: AttributeSet? = null, defStyleAttr: Int = 0 ): View(context, attributeSet, defStyleAttr) { companion object{ const val STATE_NORMAL = 0 const val STATE_DRAGGING = 1 const val STATE_SETTING = 2 const val STATE_FINISHED = 3 } // 狀態(tài) private var mState = STATE_NORMAL /** * 紅點(diǎn)半徑占控件寬高的比例 */ var domPercent = 0.25f /** * 紅點(diǎn)消失的長度占最短寬高的比例 */ var disappearPercent = 0.25f /** * 消失回調(diào) */ var listener: OnDisappearListener? = null // 半徑 private var mDomRadius: Float = 0f // 消失長度 private var mDisappearLength = 0f // 滑動(dòng)距離和移動(dòng)距離的縮放比例 private val mDraggingScale = 0.5f // 圓心所在位置 private var mRadiusX = 0f private var mRadiusY = 0f // 上一次touch的點(diǎn) private var mLastX = 0f private var mLastY = 0f // 繪制拖拽時(shí)的路徑 private val path = Path() // 恢復(fù)的屬性動(dòng)畫 private val animator = ValueAnimator.ofFloat(0f, 1f) // 畫筆 private val mPaint = Paint().apply { strokeWidth = 5f color = Color.RED style = Paint.Style.FILL flags = Paint.ANTI_ALIAS_FLAG } /** * 重置 */ fun reset() { mState = STATE_NORMAL mRadiusX = width / 2f mRadiusY = height / 2f invalidate() } override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { super.onMeasure(widthMeasureSpec, heightMeasureSpec) val width = getDefaultSize(100, widthMeasureSpec) val height = getDefaultSize(100, heightMeasureSpec) // 計(jì)算得到半徑 mDomRadius = (if (width < height) width else height) * domPercent mRadiusX = width / 2f mRadiusY = height / 2f // 消失長度 mDisappearLength = (if (width < height) width else height) * disappearPercent setMeasuredDimension(width, height) } override fun onTouchEvent(event: MotionEvent): Boolean { // 結(jié)束了不應(yīng)該接受事件,通過設(shè)置OnClickListener使用reset去重置 if (mState == STATE_FINISHED) { if (event.action == MotionEvent.ACTION_DOWN) performClick() else return true } when(event.action) { MotionEvent.ACTION_DOWN -> { mLastX = event.x mLastY = event.y // 設(shè)置中或者拖拽時(shí),快速重新按下,應(yīng)該再次接手動(dòng)畫 if(mState != STATE_NORMAL) { animator.removeAllListeners() animator.cancel() } mState = STATE_DRAGGING } MotionEvent.ACTION_MOVE -> { // 注意canvas移動(dòng)和手指移動(dòng)是一致的,view的scroll移動(dòng)的是窗口 val dx = event.x - mLastX val dy = event.y - mLastY // 移動(dòng)圓心 mRadiusX += dx * mDraggingScale mRadiusY += dy * mDraggingScale mLastX = event.x mLastY = event.y // 請(qǐng)求重繪 invalidate() } MotionEvent.ACTION_UP -> { mState = STATE_SETTING // 這里用屬性動(dòng)畫模擬拖拽,回到初始圓心 val upRadiusX = mRadiusX val upRadiusY = mRadiusY animator.addUpdateListener { // 根據(jù)比例,按直線移動(dòng)圓心到中點(diǎn) val progress = it.animatedValue as Float mRadiusX = upRadiusX + (width / 2f - upRadiusX) * progress mRadiusY = upRadiusY + (height / 2f - upRadiusY) * progress invalidate() } animator.addListener(onEnd = { mState = STATE_NORMAL }) animator.duration = 100 animator.start() } } return true } @Suppress("RedundantOverride") override fun performClick(): Boolean { return super.performClick() } override fun onDraw(canvas: Canvas) { super.onDraw(canvas) when(mState) { STATE_NORMAL -> { // 正常狀態(tài)是一個(gè)圓 canvas.drawCircle(width / 2f, height / 2f, mDomRadius, mPaint) } STATE_DRAGGING, STATE_SETTING -> { // 圓心和中點(diǎn)連線相對(duì)于X軸的夾角,注意atan2是四象限敏感[-PI, PI],atan范圍為[-PI/2, PI/2] val radiansLine = atan2((mRadiusY - height / 2f).toDouble(), (mRadiusX - width /2f).toDouble()).toFloat() // 圓心和中點(diǎn)連線的長度,通過角度算,分母為零為什么沒問題? val lineLength = (mRadiusX - width /2f) / cos(radiansLine) // 判斷是否達(dá)到消失要求,如果消失不應(yīng)該再繪制 if (lineLength > mDisappearLength) { mState = STATE_FINISHED listener?.onDisappear() return } // 以圓心為頂點(diǎn),切點(diǎn)、圓心、中心的夾角值,是一個(gè)正值 val radiansCenter = asin(mDomRadius / lineLength) // 切點(diǎn)和中心連線長度 val length = lineLength * cos(radiansCenter) // 由角度獲取兩個(gè)切點(diǎn)的坐標(biāo)值 val x1 = width /2f + length * cos(radiansLine + radiansCenter) val y1 = height / 2f + length * sin(radiansLine + radiansCenter) val x2 = width /2f + length * cos(radiansLine - radiansCenter) val y2 = height / 2f + length * sin(radiansLine - radiansCenter) // 繪制 // 普通代碼,一個(gè)圓加三角形 // canvas.drawCircle(mRadiusX, mRadiusY, mDomRadius, mPaint) // path.reset() // path.moveTo(x1, y1) // path.lineTo(width / 2f, height / 2f) // path.lineTo(x2, y2) // path.close() // 強(qiáng)行貝塞爾曲線 // 先用完整的圓覆蓋lineLength < 2 * mDomRadius的情況,大于時(shí)圓會(huì)被覆蓋 canvas.drawCircle(mRadiusX, mRadiusY, mDomRadius, mPaint) path.reset() path.moveTo(x1, y1) // 擬合圓弧,三階貝塞爾曲線,控制點(diǎn)在圓心和中點(diǎn)連線的圓外 var tempX1 = x1 + (length * cos(radiansLine + radiansCenter)) var tempY1 = y1 + ( length * sin(radiansLine + radiansCenter)) var tempX2 = x2 + (length * cos(radiansLine - radiansCenter)) var tempY2 = y2 + ( length * sin(radiansLine - radiansCenter)) // 接近圓不是圓 path.cubicTo(tempX1, tempY1, tempX2, tempY2, x2, y2) // 尾焰,第一個(gè)控制點(diǎn)在切線延長線上,第二個(gè)控制點(diǎn)在圓心連線上(越短尾越尖) tempX1 = x2 - length * cos(radiansLine - radiansCenter) tempY1 = y2 - length * sin(radiansLine - radiansCenter) tempX2 = width / 2f + (lineLength * 0.25f * cos(radiansLine)) tempY2 = height / 2f + (lineLength * 0.25f * sin(radiansLine)) // 第一條 path.cubicTo(tempX1, tempY1, tempX2, tempY2, width / 2f, height / 2f) // 另一段 tempX1 = tempX2 tempY1 = tempY2 tempX2 = x1 - (length * cos(radiansLine + radiansCenter)) tempY2 = y1 - ( length * sin(radiansLine + radiansCenter)) path.cubicTo(tempX1, tempY1, tempX2, tempY2, x1, y1) path.close() canvas.drawPath(path, mPaint) } STATE_FINISHED -> {} } // 這里便于調(diào)試,把消失范圍畫一下,多加一只畫筆,省的麻煩 canvas.drawCircle(width / 2f, height / 2f, mDisappearLength, tempPaint) } private val tempPaint = Paint().apply { strokeWidth = 3f style = Paint.Style.STROKE color = Color.LTGRAY pathEffect = DashPathEffect(floatArrayOf(10f, 10f), 0f) flags = Paint.ANTI_ALIAS_FLAG } interface OnDisappearListener{ fun onDisappear() } }
主要問題
關(guān)于onMeasure、onTouchEvent以及onDraw的內(nèi)容就不講了,這里已經(jīng)是第十篇自定義view的文章了,下面主要介紹下貝塞爾曲線繪制水滴狀的功能。
簡單畫法
這里最簡單的畫法就是用一個(gè)圓和一個(gè)三角形解決了。每次移動(dòng)對(duì)小圓點(diǎn)移動(dòng),然后計(jì)算得到view中心在圓上的兩個(gè)切點(diǎn),將兩個(gè)切點(diǎn)和view中心圍起來畫一個(gè)實(shí)心的三角形,組合起來的效果就是一個(gè)近似的小水滴了。
使用貝塞爾曲線
要實(shí)現(xiàn)更逼真的效果,使用直線是肯定不行的了,這里就要用到曲線了。首先想到的就是弧線了,可是用弧線和上面的圓是沒去別的,后面我就直接全用貝塞爾曲線做了。
我這把這個(gè)水滴形狀的小紅點(diǎn)分了三段,都是用三階的貝塞爾曲線畫的,繪制的時(shí)候最重要的就是找控制點(diǎn)了。首先要知道貝塞爾曲線的臨近控制點(diǎn)和端點(diǎn)的連線,就是曲線在該端點(diǎn)的切線,要保證三段線的連續(xù),保證三段線在同一端點(diǎn)的切線一致就行。這里最上面的那段類似圓弧的曲線,就取了切線延長線上的點(diǎn)作為控制點(diǎn),尾焰那段取切線內(nèi)上的點(diǎn),這樣在(x1, y1)(x2, y2)上就連續(xù)了,至于控制點(diǎn)距離端點(diǎn)距離取值的大小就試著取看效果了。剩下在view中點(diǎn)那側(cè)的控制點(diǎn),就取在中點(diǎn)和圓心上,這樣水滴的尾巴看起來就順眼。
幾個(gè)控制點(diǎn)的選取和展現(xiàn)的效果相關(guān)性很大,我覺得我選的點(diǎn)看起來還行。
到此這篇關(guān)于Android自定義View繪制貝塞爾曲線中小紅點(diǎn)的方法的文章就介紹到這了,更多相關(guān)Android貝塞爾曲線內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Android Studio格式化(Format)代碼快捷鍵介紹
這篇文章主要介紹了Android Studio格式化(Format)代碼快捷鍵,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-01-01詳解Android中Intent對(duì)象與Intent Filter過濾匹配過程
這篇文章主要介紹了Android中Intent對(duì)象與Intent Filter過濾匹配過程,感興趣的小伙伴們可以參考一下2015-12-12Android實(shí)現(xiàn)退出時(shí)關(guān)閉所有Activity的方法
這篇文章主要介紹了Android實(shí)現(xiàn)退出時(shí)關(guān)閉所有Activity的方法,主要通過自定義類CloseActivityClass實(shí)現(xiàn)這一功能,需要的朋友可以參考下2014-09-09View事件分發(fā)原理和ViewPager+ListView嵌套滑動(dòng)沖突
這篇文章主要介紹了View事件分發(fā)原理和ViewPager+ListView嵌套滑動(dòng)沖突,文章圍繞主題展開詳細(xì)的內(nèi)容介紹,具有一定的參考價(jià),需要的小伙伴可以參考一下2022-05-05Android LayoutTransiton實(shí)現(xiàn)簡單的錄制按鈕
這篇文章主要介紹了Android LayoutTransiton實(shí)現(xiàn)簡單的錄制按鈕,主要實(shí)現(xiàn)開始,暫停,停止和顯示錄制時(shí)間長度,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-06-06Android顯示網(wǎng)絡(luò)圖片實(shí)例
這篇文章主要介紹了Android顯示網(wǎng)絡(luò)圖片的方法,以實(shí)例形式展示了Android程序顯示網(wǎng)絡(luò)圖片的方法,非常具有實(shí)用價(jià)值,需要的朋友可以參考下2014-10-10Android中SeekBar拖動(dòng)條控件使用方法詳解
這篇文章主要介紹了Android中SeekBar拖動(dòng)條控件的使用方法,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-08-08Android 中Notification彈出通知實(shí)現(xiàn)代碼
NotificationManager 是狀態(tài)欄通知的管理類,負(fù)責(zé)發(fā)通知、清除通知等操作。接下來通過本文給大家介紹Android 中Notification彈出通知實(shí)現(xiàn)代碼,需要的的朋友參考下吧2017-08-08