Android自定義View繪制貝塞爾曲線中小紅點的方法
前言
上一篇文章用扇形圖練習了一下安卓的多點觸控,實現(xiàn)了單指旋轉(zhuǎn)、二指放大、三指移動,四指以上同時按下進行復位的功能。今天這篇文章用很多應用常見的小紅點,來練習一下貝塞爾曲線的使用。
需求
這里想法來自QQ的拖動小紅點取消顯示聊天條數(shù)功能,不過好像是記憶里的了,現(xiàn)在看了下好像效果變了。總而言之,就是一個小圓點,拖動的時候變成水滴狀,超過一定范圍后觸發(fā)消失回調(diào),核心思想如下:
1、一個正方形view,中間是小紅點,小紅點距離邊框有一定距離
2、拖動小紅點,小紅點會變形,并產(chǎn)生尾焰效果
3、釋放時,如果在設定范圍外小紅點消失,范圍內(nèi)則恢復
效果圖
這里效果在距離小的時候,還是不錯的,當移動范圍過大時,雖然水滴狀的曲線還是連續(xù)的,但是變形嚴重了,不過這個功能并不需要拖動太長距離把,只要限定好消失范圍,還是能滿足要求的。

代碼
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
/**
* 拖拽消失的小紅點
*
* @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
/**
* 紅點半徑占控件寬高的比例
*/
var domPercent = 0.25f
/**
* 紅點消失的長度占最短寬高的比例
*/
var disappearPercent = 0.25f
/**
* 消失回調(diào)
*/
var listener: OnDisappearListener? = null
// 半徑
private var mDomRadius: Float = 0f
// 消失長度
private var mDisappearLength = 0f
// 滑動距離和移動距離的縮放比例
private val mDraggingScale = 0.5f
// 圓心所在位置
private var mRadiusX = 0f
private var mRadiusY = 0f
// 上一次touch的點
private var mLastX = 0f
private var mLastY = 0f
// 繪制拖拽時的路徑
private val path = Path()
// 恢復的屬性動畫
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)
// 計算得到半徑
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é)束了不應該接受事件,通過設置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
// 設置中或者拖拽時,快速重新按下,應該再次接手動畫
if(mState != STATE_NORMAL) {
animator.removeAllListeners()
animator.cancel()
}
mState = STATE_DRAGGING
}
MotionEvent.ACTION_MOVE -> {
// 注意canvas移動和手指移動是一致的,view的scroll移動的是窗口
val dx = event.x - mLastX
val dy = event.y - mLastY
// 移動圓心
mRadiusX += dx * mDraggingScale
mRadiusY += dy * mDraggingScale
mLastX = event.x
mLastY = event.y
// 請求重繪
invalidate()
}
MotionEvent.ACTION_UP -> {
mState = STATE_SETTING
// 這里用屬性動畫模擬拖拽,回到初始圓心
val upRadiusX = mRadiusX
val upRadiusY = mRadiusY
animator.addUpdateListener {
// 根據(jù)比例,按直線移動圓心到中點
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)是一個圓
canvas.drawCircle(width / 2f, height / 2f, mDomRadius, mPaint)
}
STATE_DRAGGING, STATE_SETTING -> {
// 圓心和中點連線相對于X軸的夾角,注意atan2是四象限敏感[-PI, PI],atan范圍為[-PI/2, PI/2]
val radiansLine = atan2((mRadiusY - height / 2f).toDouble(),
(mRadiusX - width /2f).toDouble()).toFloat()
// 圓心和中點連線的長度,通過角度算,分母為零為什么沒問題?
val lineLength = (mRadiusX - width /2f) / cos(radiansLine)
// 判斷是否達到消失要求,如果消失不應該再繪制
if (lineLength > mDisappearLength) {
mState = STATE_FINISHED
listener?.onDisappear()
return
}
// 以圓心為頂點,切點、圓心、中心的夾角值,是一個正值
val radiansCenter = asin(mDomRadius / lineLength)
// 切點和中心連線長度
val length = lineLength * cos(radiansCenter)
// 由角度獲取兩個切點的坐標值
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)
// 繪制
// 普通代碼,一個圓加三角形
// canvas.drawCircle(mRadiusX, mRadiusY, mDomRadius, mPaint)
// path.reset()
// path.moveTo(x1, y1)
// path.lineTo(width / 2f, height / 2f)
// path.lineTo(x2, y2)
// path.close()
// 強行貝塞爾曲線
// 先用完整的圓覆蓋lineLength < 2 * mDomRadius的情況,大于時圓會被覆蓋
canvas.drawCircle(mRadiusX, mRadiusY, mDomRadius, mPaint)
path.reset()
path.moveTo(x1, y1)
// 擬合圓弧,三階貝塞爾曲線,控制點在圓心和中點連線的圓外
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)
// 尾焰,第一個控制點在切線延長線上,第二個控制點在圓心連線上(越短尾越尖)
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()
}
}主要問題
關于onMeasure、onTouchEvent以及onDraw的內(nèi)容就不講了,這里已經(jīng)是第十篇自定義view的文章了,下面主要介紹下貝塞爾曲線繪制水滴狀的功能。
簡單畫法
這里最簡單的畫法就是用一個圓和一個三角形解決了。每次移動對小圓點移動,然后計算得到view中心在圓上的兩個切點,將兩個切點和view中心圍起來畫一個實心的三角形,組合起來的效果就是一個近似的小水滴了。
使用貝塞爾曲線
要實現(xiàn)更逼真的效果,使用直線是肯定不行的了,這里就要用到曲線了。首先想到的就是弧線了,可是用弧線和上面的圓是沒去別的,后面我就直接全用貝塞爾曲線做了。
我這把這個水滴形狀的小紅點分了三段,都是用三階的貝塞爾曲線畫的,繪制的時候最重要的就是找控制點了。首先要知道貝塞爾曲線的臨近控制點和端點的連線,就是曲線在該端點的切線,要保證三段線的連續(xù),保證三段線在同一端點的切線一致就行。這里最上面的那段類似圓弧的曲線,就取了切線延長線上的點作為控制點,尾焰那段取切線內(nèi)上的點,這樣在(x1, y1)(x2, y2)上就連續(xù)了,至于控制點距離端點距離取值的大小就試著取看效果了。剩下在view中點那側(cè)的控制點,就取在中點和圓心上,這樣水滴的尾巴看起來就順眼。
幾個控制點的選取和展現(xiàn)的效果相關性很大,我覺得我選的點看起來還行。
到此這篇關于Android自定義View繪制貝塞爾曲線中小紅點的方法的文章就介紹到這了,更多相關Android貝塞爾曲線內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
Android Studio格式化(Format)代碼快捷鍵介紹
這篇文章主要介紹了Android Studio格式化(Format)代碼快捷鍵,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-01-01
詳解Android中Intent對象與Intent Filter過濾匹配過程
這篇文章主要介紹了Android中Intent對象與Intent Filter過濾匹配過程,感興趣的小伙伴們可以參考一下2015-12-12
Android實現(xiàn)退出時關閉所有Activity的方法
這篇文章主要介紹了Android實現(xiàn)退出時關閉所有Activity的方法,主要通過自定義類CloseActivityClass實現(xiàn)這一功能,需要的朋友可以參考下2014-09-09
View事件分發(fā)原理和ViewPager+ListView嵌套滑動沖突
這篇文章主要介紹了View事件分發(fā)原理和ViewPager+ListView嵌套滑動沖突,文章圍繞主題展開詳細的內(nèi)容介紹,具有一定的參考價,需要的小伙伴可以參考一下2022-05-05
Android LayoutTransiton實現(xiàn)簡單的錄制按鈕
這篇文章主要介紹了Android LayoutTransiton實現(xiàn)簡單的錄制按鈕,主要實現(xiàn)開始,暫停,停止和顯示錄制時間長度,具有一定的參考價值,感興趣的小伙伴們可以參考一下2018-06-06
Android 中Notification彈出通知實現(xiàn)代碼
NotificationManager 是狀態(tài)欄通知的管理類,負責發(fā)通知、清除通知等操作。接下來通過本文給大家介紹Android 中Notification彈出通知實現(xiàn)代碼,需要的的朋友參考下吧2017-08-08

