厉害了!仿QQ拖拽效果
刘望舒
共 36887字,需浏览 74分钟
·
2022-12-17 16:02
大家好,我是皇叔,最近开了一个安卓进阶涨薪训练营,可以帮助大家突破技术&职场瓶颈,从而度过难关,进入心仪的公司。
详情见文章:没错!皇叔开了个训练营
作者:史大拿
https://blog.csdn.net/weixin_44819566?type=blog
前言
android studio: 4.1.3 kotlin version:1.5.0 gradle: gradle-6.5-bin.zip
基础绘制
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
// 最大范围(半径),超出这个范围大圆不显示
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)
}
}
当大圆超出辅助圆范围的时候,大圆得“爆炸”, 如果大圆未超出辅助圆内的话,大圆得回弹回去~
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 // 消费事件
}
// 标记是否选中了大圆
var isMove = false
@SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(event: MotionEvent): Boolean {
when (event.action) {
MotionEvent.ACTION_DOWN -> {
// 判断当前点击区域是否在大圆范围内
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 // 消费事件
}
// 判断一个点是否在另一个点内
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)
// 大圆位置是否在辅助圆内
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()
// 总长度
var ratio = d / MAX_RADIUS
// 如果当前比例 > 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
控制点
/*
* 作者:史大拿
* @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)
// 控制点
val controlPointX = current.x / 2 + smallPointF.x
val controlPointY = current.y / 2 + smallPointF.y
val path = Path()
path.moveTo(p1X.toFloat(), p1Y.toFloat()) // 移动到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)
// 大圆位置是否在辅助圆内
if (bigPointF.contains(smallPointF, MAX_RADIUS)) {
// 绘制大圆
canvas.drawCircle(bigPointF.x, bigPointF.y, BIG_RADIUS, paint)
// 绘制贝塞尔
drawBezier(canvas,smallRadius, BIG_RADIUS)
}
// 绘制辅助圆
...
}
拖动回弹
private fun bigAnimator(): ValueAnimator {
return ObjectAnimator.ofObject(this, "bigPointF", PointFEvaluator(),
PointF(width / 2f, height / 2f)).apply {
duration = 400
interpolator = OvershootInterpolator(3f) // 设置回弹迭代器
}
}
AccelerateDecelerateInterpolator 动画从开始到结束,变化率是先加速后减速的过程。 AccelerateInterpolator 动画从开始到结束,变化率是一个加速的过程。 AnticipateInterpolator 开始的时候向后,然后向前甩 AnticipateOvershootInterpolator 开始的时候向后,然后向前甩一定值后返回最后的值 BounceInterpolator 动画结束的时候弹起 CycleInterpolator 动画从开始到结束,变化率是循环给定次数的正弦曲线。 DecelerateInterpolator 动画从开始到结束,变化率是一个减速的过程。 LinearInterpolator 以常量速率改变 OvershootInterpolator 结束时候向反方向甩某段距离
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 -> {
// 大圆是否在辅助圆范围内
if (bigPointF.contains(smallPointF, MAX_RADIUS)) {
// 回弹
bigAnimator().start()
} else {
// 爆炸
}
}
}
invalidate()
return true // 消费事件
}
爆炸效果
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
}
// 爆炸下标
var explodeIndex = -1
set(value) {
field = value
invalidate()
}
// 属性动画修改爆炸下标,最后一帧的时候回到 -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 -> {
// 大圆是否在辅助圆范围内
if (bigPointF.contains(smallPointF, MAX_RADIUS)) {
// 回弹
....
} else {
// 绘制爆炸效果
explodeAnimator.start()
// 爆炸效果结束后,将图片移动到原始位置
explodeAnimator.doOnEnd {
bigPointF.x = width / 2f
bigPointF.y = height / 2f
}
}
}
}
invalidate()
return true // 消费事件
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
// 绘制小圆
...
// 大圆位置是否在辅助圆内
if (bigPointF.contains(smallPointF, MAX_RADIUS)) {
// 绘制大圆
....
// 绘制贝塞尔
...
}
// 绘制爆炸效果
if (explodeIndex != -1) {
// 圆和bitmap坐标系不同
// 圆的坐标系是中心点
// bitmap的坐标系是左上角
canvas.drawBitmap(explodeImages[explodeIndex],
bigPointF.x - BIG_RADIUS * 2,
bigPointF.y - BIG_RADIUS * 2,
paint)
}
// 绘制辅助圆
....
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
// 绘制小圆
...
// 大圆位置是否在辅助圆内
if (bigPointF.contains(smallPointF, MAX_RADIUS)) {
// 绘制大圆
....
// 绘制贝塞尔
...
}
// 绘制爆炸效果
if (explodeIndex != -1) {
// 圆和bitmap坐标系不同
// 圆的坐标系是中心点
// bitmap的坐标系是左上角
canvas.drawBitmap(explodeImages[explodeIndex],
bigPointF.x - BIG_RADIUS * 2,
bigPointF.y - BIG_RADIUS * 2,
paint)
}
// 绘制辅助圆
....
}
效果二
通过setOnTouchListener{} 可以实现对View的触摸事件监听 在ACTION_DOWN事件时候,将当前View隐藏,通过WindowManager添加一个拖拽的气泡View((就是上面写好的), 并且给气泡View初始化好位置 在ACTION_MOVE 事件中不断的更新大圆的位置 在ACTION_UP事件的时候,判断是否在辅助圆内,然后进行回弹或者爆炸. 并且将拖拽气泡从WindowManager总删除掉
初始化位置不对 当拖动的时候,状态栏变成了黑色
小圆初始点: 小圆初始点既是当前view的中心点 大圆初始点: 大圆初始点即是当前按下的位置
// location[0] = x;
// location[1] = y;
val location = IntArray(2)
view.getLocationInWindow(location) // 获取当前窗口的绝对坐标
#BlogDragBubbleUtil.kt
when (event.action) {
MotionEvent.ACTION_DOWN -> {
val location = IntArray(2)
view.getLocationInWindow(location) // 获取当前窗口的绝对坐标
dragView.initPointF(
location[0].toFloat() + view.width / 2,
location[1].toFloat()+ view.height / 2 ,
event.rawX,
event.rawY
)
}
....
}
// 获取状态栏高度
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
}
// 屏幕状态栏高度
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 // 设置windowManager为透明
// }
WindowManager.LayoutParams(screenWidth,
screenHeight,
WindowManager.LayoutParams.TYPE_APPLICATION,
WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN,
PixelFormat.TRANSPARENT // 设置透明度
)
}
宽 高 透明度
// 从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(..)
// 设置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 -> {
/// 判断大圆是否在辅助圆内
if (dragView.isContains()) {
// 回弹效果
dragView.bigAnimator().run {
start()
doOnEnd { // 结束回调
// 显示View
view.visibility = View.VISIBLE
// 删除
windowManager.removeView(dragView)
dragView.upDataBitMap(null, bitMap.width.toFloat())
}
}
} else {
// 爆炸效果
}
}
MotionEvent.ACTION_UP -> {
/// 判断大圆是否在辅助圆内
if (dragView.isContains()) {
// 回弹效果
dragView.bigAnimator().run {
start()
doOnEnd { // 结束回调
// 显示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() // 开启动画
doOnEnd { // 结束动画回调
windowManager.removeView(dragView)
view.visibility = View.VISIBLE
}
}
}
}
private val explodeImages by lazy {
val list = arrayListOf<Bitmap>()
val width = bitMapWidth // 设置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 -> {
// 和父容器抢焦点
view.parent.requestDisallowInterceptTouchEvent(true)
}
...
}
为了防止失联,欢迎关注我防备的小号
微信改了推送机制,真爱请星标本公号👇
评论