厲害了!仿QQ拖拽效果
大家好,我是皇叔,最近開(kāi)了一個(gè)安卓進(jìn)階漲薪訓(xùn)練營(yíng),可以幫助大家突破技術(shù)&職場(chǎng)瓶頸,從而度過(guò)難關(guān),進(jìn)入心儀的公司。
詳情見(jiàn)文章:沒(méi)錯(cuò)!皇叔開(kāi)了個(gè)訓(xùn)練營(yíng)
作者:史大拿
https://blog.csdn.net/weixin_44819566?type=blog
前言
android studio: 4.1.3 kotlin version:1.5.0 gradle: gradle-6.5-bin.zip


基礎(chǔ)繪制
class TempView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0,
) : View(context, attrs, defStyleAttr) {
companion object {
// 大圓半徑
private val BIG_RADIUS = 50.dp
// 小圓半徑
private val SMALL_RADIUS = BIG_RADIUS * 0.618f
// 最大范圍(半徑),超出這個(gè)范圍大圓不顯示
private val MAX_RADIUS = 150.dp
}
private val paint = Paint().apply {
color = Color.RED
}
// 大圓初始位置
private val bigPointF by lazy { PointF(width / 2f + 300, height / 2f) }
// 小圓初始位置
private val smallPointF by lazy { PointF(width / 2f, height / 2f) }
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
paint.color = Color.RED
// 繪制大圓
canvas.drawCircle(bigPointF.x, bigPointF.y, BIG_RADIUS, paint)
// 繪制小圓
canvas.drawCircle(smallPointF.x, smallPointF.y, SMALL_RADIUS, paint)
// 繪制輔助圓
paint.color = Color.argb(20, 255, 0, 0)
canvas.drawCircle(smallPointF.x, smallPointF.y, MAX_RADIUS, paint)
}
}

當(dāng)大圓超出輔助圓范圍的時(shí)候,大圓得“爆炸”, 如果大圓未超出輔助圓內(nèi)的話,大圓得回彈回去~
override fun onTouchEvent(event: MotionEvent): Boolean {
when (event.action) {
MotionEvent.ACTION_DOWN -> {
}
MotionEvent.ACTION_MOVE -> {
bigPointF.x = event.x
bigPointF.y = event.y
}
MotionEvent.ACTION_UP -> {
}
}
invalidate()
return true // 消費(fèi)事件
}

// 標(biāo)記是否選中了大圓
var isMove = false
@SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(event: MotionEvent): Boolean {
when (event.action) {
MotionEvent.ACTION_DOWN -> {
// 判斷當(dāng)前點(diǎn)擊區(qū)域是否在大圓范圍內(nèi)
isMove = bigPointF.contains(PointF(event.x, event.y), BIG_RADIUS)
}
MotionEvent.ACTION_MOVE -> {
if (isMove) {
bigPointF.x = event.x
bigPointF.y = event.y
}
}
}
invalidate()
return true // 消費(fèi)事件
}
// 判斷一個(gè)點(diǎn)是否在另一個(gè)點(diǎn)內(nèi)
fun PointF.contains(b: PointF, bPadding: Float = 0f): Boolean {
val isX = this.x <= b.x + bPadding && this.x >= b.x - bPadding
val isY = this.y <= b.y + bPadding && this.y >= b.y - bPadding
return isX && isY
}

override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
// 大圓位置是否在輔助圓內(nèi)
if(bigPointF.contains(smallPointF, MAX_RADIUS)){
// 繪制大圓
canvas.drawCircle(bigPointF.x, bigPointF.y, BIG_RADIUS, paint)
}
// 繪制小圓
...
// 繪制輔助圓
...
}


dx = 大圓.x - 小圓.x dy = 大圓.y - 小圓.y
// 小圓與大圓之間的距離
private fun distance(): Float {
val current = bigPointF - smallPointF
return sqrt(current.x.toDouble().pow(2.0) + (current.y.toDouble().pow(2.0))).toFloat()
}
// 大圓與小圓之間的距離
val d = distance()
// 總長(zhǎng)度
var ratio = d / MAX_RADIUS
// 如果當(dāng)前比例 > 0.618 那么就讓=0.618
if (ratio > 0.618) {
ratio = 0.618f
}
//小圓半徑
private val SMALL_RADIUS = BIG_RADIUS
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
// 繪制大圓
...
// 兩圓之間的距離
val d = distance()
var ratio = d / MAX_RADIUS
if (ratio > 0.618) {
ratio = 0.618f
}
// 小圓半徑
val smallRadius = SMALL_RADIUS - SMALL_RADIUS * ratio
// 繪制小圓
canvas.drawCircle(smallPointF.x, smallPointF.y, smallRadius, paint)
// 繪制輔助圓
...
}


P1

角A.x = 小圓.x + BC; 角A.y = 小圓.y - AC ;
角C = 90度; 角ABD = 90度
BC = AB * sin(角A) AC = AB * cos(角A)
p1X = 小圓.x + 小圓半徑 * sin(角A) p1Y = 小圓.y - 小圓半徑 * cos(角A)
P2

角E.x = 大圓.x + DG 角E.y = 大圓.y + EG
P2.x =大圓.x + DE * sin(角E) P2.y = 大圓.y - DE * cos(角E)
P3

角K.x = 小圓.x - KH 角K.y = 小圓.y - BH
P3.x = 小圓.x - KH P3.y = 小圓.y - BH
P4

角A.x = 大圓.x - CD 角A.y = 大圓.y + AC
P4.x = 大圓.x - CD p4.y = 大圓.y - AC
控制點(diǎn)
/*
* 作者:史大拿
* @param smallRadius: 小圓半徑
* @param bigRadius: 大圓半徑
*/
private fun drawBezier(canvas: Canvas, smallRadius: Float, bigRadius: Float) {
val current = bigPointF - smallPointF
val BF = current.y.toDouble()
val FD = current.x.toDouble()
//
val BDF = atan(BF / FD)
val p1X = smallPointF.x + smallRadius * sin(BDF)
val p1Y = smallPointF.y - smallRadius * cos(BDF)
val p2X = bigPointF.x + bigRadius * sin(BDF)
val p2Y = bigPointF.y - bigRadius * cos(BDF)
val p3X = smallPointF.x - smallRadius * sin(BDF)
val p3Y = smallPointF.y + smallRadius * cos(BDF)
val p4X = bigPointF.x - bigRadius * sin(BDF)
val p4Y = bigPointF.y + bigRadius * cos(BDF)
// 控制點(diǎn)
val controlPointX = current.x / 2 + smallPointF.x
val controlPointY = current.y / 2 + smallPointF.y
val path = Path()
path.moveTo(p1X.toFloat(), p1Y.toFloat()) // 移動(dòng)到p1位置
path.quadTo(controlPointX, controlPointY, p2X.toFloat(), p2Y.toFloat()) // 繪制貝塞爾
path.lineTo(p4X.toFloat(), p4Y.toFloat()) // 連接到p4
path.quadTo(controlPointX, controlPointY, p3X.toFloat(), p3Y.toFloat()) // 繪制貝塞爾
path.close() // 連接到p1
canvas.drawPath(path, paint)
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
paint.color = Color.RED
// 兩圓之間的距離
val d = distance()
var ratio = d / MAX_RADIUS
if (ratio > 0.618) {
ratio = 0.618f
}
// 小圓半徑
val smallRadius = SMALL_RADIUS - SMALL_RADIUS * ratio
// 繪制小圓
canvas.drawCircle(smallPointF.x, smallPointF.y, smallRadius, paint)
// 大圓位置是否在輔助圓內(nèi)
if (bigPointF.contains(smallPointF, MAX_RADIUS)) {
// 繪制大圓
canvas.drawCircle(bigPointF.x, bigPointF.y, BIG_RADIUS, paint)
// 繪制貝塞爾
drawBezier(canvas,smallRadius, BIG_RADIUS)
}
// 繪制輔助圓
...
}


拖動(dòng)回彈
private fun bigAnimator(): ValueAnimator {
return ObjectAnimator.ofObject(this, "bigPointF", PointFEvaluator(),
PointF(width / 2f, height / 2f)).apply {
duration = 400
interpolator = OvershootInterpolator(3f) // 設(shè)置回彈迭代器
}
}
AccelerateDecelerateInterpolator 動(dòng)畫(huà)從開(kāi)始到結(jié)束,變化率是先加速后減速的過(guò)程。 AccelerateInterpolator 動(dòng)畫(huà)從開(kāi)始到結(jié)束,變化率是一個(gè)加速的過(guò)程。 AnticipateInterpolator 開(kāi)始的時(shí)候向后,然后向前甩 AnticipateOvershootInterpolator 開(kāi)始的時(shí)候向后,然后向前甩一定值后返回最后的值 BounceInterpolator 動(dòng)畫(huà)結(jié)束的時(shí)候彈起 CycleInterpolator 動(dòng)畫(huà)從開(kāi)始到結(jié)束,變化率是循環(huán)給定次數(shù)的正弦曲線。 DecelerateInterpolator 動(dòng)畫(huà)從開(kāi)始到結(jié)束,變化率是一個(gè)減速的過(guò)程。 LinearInterpolator 以常量速率改變 OvershootInterpolator 結(jié)束時(shí)候向反方向甩某段距離
private val bigPointF by lazy { PointF(width / 2f + 300, height / 2f) }
var bigPointF = PointF(0f, 0f)
set(value) {
field = value
invalidate()
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh
bigPointF.x = width / 2f
bigPointF.y = height / 2f
}
override fun onTouchEvent(event: MotionEvent): Boolean {
when (event.action) {
....
MotionEvent.ACTION_UP -> {
// 大圓是否在輔助圓范圍內(nèi)
if (bigPointF.contains(smallPointF, MAX_RADIUS)) {
// 回彈
bigAnimator().start()
} else {
// 爆炸
}
}
}
invalidate()
return true // 消費(fèi)事件
}

爆炸效果
private val explodeImages by lazy {
val list = arrayListOf<Bitmap>()
// BIG_RADIUS = 大圓半徑
val width = BIG_RADIUS * 2 * 2
list.add(getBitMap(R.mipmap.explode_0, width.toInt()))
list.add(getBitMap(R.mipmap.explode_1, width.toInt()))
list.add(getBitMap(R.mipmap.explode_2, width.toInt()))
list.add(getBitMap(R.mipmap.explode_3, width.toInt()))
list.add(getBitMap(R.mipmap.explode_4, width.toInt()))
list.add(getBitMap(R.mipmap.explode_5, width.toInt()))
list.add(getBitMap(R.mipmap.explode_5, width.toInt()))
list.add(getBitMap(R.mipmap.explode_6, width.toInt()))
list.add(getBitMap(R.mipmap.explode_7, width.toInt()))
list.add(getBitMap(R.mipmap.explode_8, width.toInt()))
list.add(getBitMap(R.mipmap.explode_9, width.toInt()))
list.add(getBitMap(R.mipmap.explode_10, width.toInt()))
list.add(getBitMap(R.mipmap.explode_11, width.toInt()))
list.add(getBitMap(R.mipmap.explode_12, width.toInt()))
list.add(getBitMap(R.mipmap.explode_13, width.toInt()))
list.add(getBitMap(R.mipmap.explode_14, width.toInt()))
list.add(getBitMap(R.mipmap.explode_15, width.toInt()))
list.add(getBitMap(R.mipmap.explode_16, width.toInt()))
list.add(getBitMap(R.mipmap.explode_17, width.toInt()))
list.add(getBitMap(R.mipmap.explode_18, width.toInt()))
list.add(getBitMap(R.mipmap.explode_19, width.toInt()))
list
}
// 爆炸下標(biāo)
var explodeIndex = -1
set(value) {
field = value
invalidate()
}
// 屬性動(dòng)畫(huà)修改爆炸下標(biāo),最后一幀的時(shí)候回到 -1
private val explodeAnimator by lazy {
ObjectAnimator.ofInt(this, "explodeIndex", 19, -1).apply {
duration = 1000
}
}
override fun onTouchEvent(event: MotionEvent): Boolean {
when (event.action) {
...
MotionEvent.ACTION_UP -> {
// 大圓是否在輔助圓范圍內(nèi)
if (bigPointF.contains(smallPointF, MAX_RADIUS)) {
// 回彈
....
} else {
// 繪制爆炸效果
explodeAnimator.start()
// 爆炸效果結(jié)束后,將圖片移動(dòng)到原始位置
explodeAnimator.doOnEnd {
bigPointF.x = width / 2f
bigPointF.y = height / 2f
}
}
}
}
invalidate()
return true // 消費(fèi)事件
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
// 繪制小圓
...
// 大圓位置是否在輔助圓內(nèi)
if (bigPointF.contains(smallPointF, MAX_RADIUS)) {
// 繪制大圓
....
// 繪制貝塞爾
...
}
// 繪制爆炸效果
if (explodeIndex != -1) {
// 圓和bitmap坐標(biāo)系不同
// 圓的坐標(biāo)系是中心點(diǎn)
// bitmap的坐標(biāo)系是左上角
canvas.drawBitmap(explodeImages[explodeIndex],
bigPointF.x - BIG_RADIUS * 2,
bigPointF.y - BIG_RADIUS * 2,
paint)
}
// 繪制輔助圓
....
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
// 繪制小圓
...
// 大圓位置是否在輔助圓內(nèi)
if (bigPointF.contains(smallPointF, MAX_RADIUS)) {
// 繪制大圓
....
// 繪制貝塞爾
...
}
// 繪制爆炸效果
if (explodeIndex != -1) {
// 圓和bitmap坐標(biāo)系不同
// 圓的坐標(biāo)系是中心點(diǎn)
// bitmap的坐標(biāo)系是左上角
canvas.drawBitmap(explodeImages[explodeIndex],
bigPointF.x - BIG_RADIUS * 2,
bigPointF.y - BIG_RADIUS * 2,
paint)
}
// 繪制輔助圓
....
}

效果二


通過(guò)setOnTouchListener{} 可以實(shí)現(xiàn)對(duì)View的觸摸事件監(jiān)聽(tīng) 在ACTION_DOWN事件時(shí)候,將當(dāng)前View隱藏,通過(guò)WindowManager添加一個(gè)拖拽的氣泡View((就是上面寫(xiě)好的), 并且給氣泡View初始化好位置 在ACTION_MOVE 事件中不斷的更新大圓的位置 在ACTION_UP事件的時(shí)候,判斷是否在輔助圓內(nèi),然后進(jìn)行回彈或者爆炸. 并且將拖拽氣泡從WindowManager總刪除掉


初始化位置不對(duì) 當(dāng)拖動(dòng)的時(shí)候,狀態(tài)欄變成了黑色
小圓初始點(diǎn): 小圓初始點(diǎn)既是當(dāng)前view的中心點(diǎn) 大圓初始點(diǎn): 大圓初始點(diǎn)即是當(dāng)前按下的位置
// location[0] = x;
// location[1] = y;
val location = IntArray(2)
view.getLocationInWindow(location) // 獲取當(dāng)前窗口的絕對(duì)坐標(biāo)
#BlogDragBubbleUtil.kt
when (event.action) {
MotionEvent.ACTION_DOWN -> {
val location = IntArray(2)
view.getLocationInWindow(location) // 獲取當(dāng)前窗口的絕對(duì)坐標(biāo)
dragView.initPointF(
location[0].toFloat() + view.width / 2,
location[1].toFloat()+ view.height / 2 ,
event.rawX,
event.rawY
)
}
....
}

// 獲取狀態(tài)欄高度
fun Context.statusBarHeight() = let {
var height = 0
val resourceId: Int = resources
.getIdentifier("status_bar_height", "dimen", "android")
if (resourceId > 0) {
height = resources.getDimensionPixelSize(resourceId)
}
height
}
// 屏幕狀態(tài)欄高度
private val statusBarHeight by lazy {
context.statusBarHeight()
}
MotionEvent.ACTION_DOWN -> {
dragView.initPointF(
location[0].toFloat() + view.width / 2,
location[1].toFloat() + view.height / 2 - statusBarHeight,
event.rawX,
event.rawY - statusBarHeight
)
}
MotionEvent.ACTION_MOVE -> {
dragView.upDataPointF(event.rawX, event.rawY - statusBarHeight)
}
private val layoutParams by lazy {
// WindowManager.LayoutParams().apply {
// format = PixelFormat.TRANSLUCENT // 設(shè)置windowManager為透明
// }
WindowManager.LayoutParams(screenWidth,
screenHeight,
WindowManager.LayoutParams.TYPE_APPLICATION,
WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN,
PixelFormat.TRANSPARENT // 設(shè)置透明度
)
}

寬 高 透明度

// 從View中獲取bitMap
fun View.getBackgroundBitMap(): Bitmap = let {
this.buildDrawingCache()
this.drawingCache
}
#BlogDragBubbleUtil.kt
// view的圖片
private val bitMap by lazy { view.getBackgroundBitMap() }
fun bind() {
view.setOnTouchListener { v, event ->
when (event.action) {
MotionEvent.ACTION_DOWN -> {
// 初始化位置
dragView.initPointF(..)
// 設(shè)置BitMap圖片
dragView.upDataBitMap(bitMap, bitMap.width.toFloat())
}
MotionEvent.ACTION_MOVE -> {
// 重新繪制大圓位置
...
}
MotionEvent.ACTION_UP -> {
// 清空bitMap圖片
dragView.upDataBitMap(null, bitMap.width.toFloat())
}
}
true
}
}
#DragView.kt
fun void onDraw(canvas: Canvas){
// 繪制小圓
// 繪制大圓
// 繪制view中的bitMap
bitMap?.let {
canvas.drawBitmap(it,
bigPointF.x - it.width / 2f,
bigPointF.y - it.height / 2f, paint)
}
// 繪制輔助圓
}
var bitMap: Bitmap? = null
var bitMapWidth = 0f
fun upDataBitMap(bitMap: Bitmap?, bitMapWidth: Float) {
this.bitMap = bitMap
this.bitMapWidth = bitMapWidth
invalidate()
}

# BlogDragBubbleUtil.kt
MotionEvent.ACTION_UP -> {
/// 判斷大圓是否在輔助圓內(nèi)
if (dragView.isContains()) {
// 回彈效果
dragView.bigAnimator().run {
start()
doOnEnd { // 結(jié)束回調(diào)
// 顯示View
view.visibility = View.VISIBLE
// 刪除
windowManager.removeView(dragView)
dragView.upDataBitMap(null, bitMap.width.toFloat())
}
}
} else {
// 爆炸效果
}
}

MotionEvent.ACTION_UP -> {
/// 判斷大圓是否在輔助圓內(nèi)
if (dragView.isContains()) {
// 回彈效果
dragView.bigAnimator().run {
start()
doOnEnd { // 結(jié)束回調(diào)
// 顯示View
view.visibility = View.VISIBLE
// 刪除
windowManager.removeView(dragView)
dragView.upDataBitMap(null, bitMap.width.toFloat())
}
}
} else {
// 爆炸效果
// 爆炸之前先清空ViewBitMap
dragView.upDataBitMap(null, bitMap.width.toFloat())
dragView.explodeAnimator.run {
start() // 開(kāi)啟動(dòng)畫(huà)
doOnEnd { // 結(jié)束動(dòng)畫(huà)回調(diào)
windowManager.removeView(dragView)
view.visibility = View.VISIBLE
}
}
}
}



private val explodeImages by lazy {
val list = arrayListOf<Bitmap>()
val width = bitMapWidth // 設(shè)置bitmap 寬度
list.add(getBitMap(R.mipmap.explode_0, width.toInt()))
... 加載20張圖片
list.add(getBitMap(R.mipmap.explode_19, width.toInt()))
list
}
fun View.getBitMap(@DrawableRes bitmap: Int = R.mipmap.user, width: Int = 640): Bitmap = let {
val options = BitmapFactory.Options()
options.inJustDecodeBounds = true
BitmapFactory.decodeResource(resources, bitmap)
options.inJustDecodeBounds = false
options.inDensity = options.outWidth
options.inTargetDensity = width
BitmapFactory.decodeResource(resources, bitmap, options)
}


view.setOnTouchListener { v, event ->
when (event.action) {
MotionEvent.ACTION_DOWN -> {
// 和父容器搶焦點(diǎn)
view.parent.requestDisallowInterceptTouchEvent(true)
}
...
}
為了防止失聯(lián),歡迎關(guān)注我防備的小號(hào)
微信改了推送機(jī)制,真愛(ài)請(qǐng)星標(biāo)本公號(hào)??
評(píng)論
圖片
表情



