本篇文章已授权微信公众号 guolin_blog (郭霖)独家发布[2022-8-26]

本系列自定义View全部选用kt

体系:mac

android studio: 4.1.3

kotlin version:1.5.0

gradle: gradle-6.5-bin.zip

废话不多说,先来看今天要完结的作用:

作用一 作用二
android 自定义View:仿QQ拖拽效果
android 自定义View:仿QQ拖拽效果

作用二是在作用一的根底上改的,能够经过一行代码,让所有控件都能实现拖拽作用!

所以先来编写作用一的代码~

根底制作

首先编写一下根底代码:

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)
    }
}

现在作用:

android 自定义View:仿QQ拖拽效果

这段代码很简单,都是一些根底api的调用,

辅佐圆的作用:

  • 当大圆超出辅佐圆规模的时分,大圆得“爆破”,

  • 假设大圆未超出辅佐圆内的话,大圆得回弹回去~

首要便是起到这样的作用.

大圆动起来

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 // 消费事情
}

大圆动起来很简单,只需求在ACTION_MOVE中一向刷新移动方位即可

辅佐图1.1:

android 自定义View:仿QQ拖拽效果

咱们想要的作用是手指按下之后,大圆跟着移动,

辅佐图1.1后半段能够看出这儿有一个小问题, 手指按什么方位小球就移动到什么方位,不是咱们想要的作用

那么咱们知道所有的事情都是在DOWN平分发出来的,

所以只需求在DOWN事情中判别当时是否点击到大圆即可,

// 标记是否选中了大圆
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 // 消费事情
}

contains是自己写的一个扩展函数:

// 判别一个点是否在另一个点内
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
}

辅佐图1.2:

android 自定义View:仿QQ拖拽效果

大圆超出辅佐圆规模就消失

有了PointF.contains() 这个扩展,任务就变得轻松起来了

只需求在制作的时分判别一下当时方位即可

override fun onDraw(canvas: Canvas) {
    super.onDraw(canvas)
     // 大圆方位是否在辅佐圆内
     if(bigPointF.contains(smallPointF, MAX_RADIUS)){
        // 制作大圆
        canvas.drawCircle(bigPointF.x, bigPointF.y, BIG_RADIUS, paint)
     }
  	// 制作小圆
     ...
    // 制作辅佐圆
    ...
}

辅佐图1.3:

android 自定义View:仿QQ拖拽效果

大圆越往外,小球越小

要想求出大圆是否越往外,那么就得先核算出当时大圆与小圆的间隔

辅佐图1.4:

android 自定义View:仿QQ拖拽效果

  • 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()
}

bigPointF – smallPointF 选用的是ktx中自带的运算符重载函数

知道大圆和小圆的间隔之后,就能够核算出份额

份额 = 间隔 / 总长度

// 大圆与小圆之间的间隔
val d = distance()
// 总长度
var ratio = d / MAX_RADIUS
// 假设当时份额 > 0.618 那么就让=0.618
if (ratio > 0.618) {
    ratio = 0.618f
}

为什么要选0.618,

0.618是黄金份额切割点,传闻选了0.618制作出来的东西会很和谐?

我一个糙人也看不出来美不美, 可能是看着更专业一点吧.

完好制作小圆代码:

//小圆半径
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)
    // 制作辅佐圆
   ...
}

辅佐图1.5:

android 自定义View:仿QQ拖拽效果

制作贝塞尔曲线

接下来只需求求出这4个点连接起来 , 看起来就像是把他们连接起来了

然后在找到一个控制点, 经过贝塞尔曲线让他略微曲折即可

辅佐图1.6:

android 自定义View:仿QQ拖拽效果

P1

辅佐图1.7:

android 自定义View:仿QQ拖拽效果

终究便是算出角A的坐标即可

现在已知

  • 角A.x = 小圆.x + BC;
  • 角A.y = 小圆.y – AC ;

Tips: 由于角A的坐标在小圆中心点上面, 在android坐标系中 角A.y = 小圆.y – AC ;

  • 角C = 90度;
  • 角ABD = 90度

角ABC + 角BAC = 90度; 角ABC +角CBD = 90度;

所以角BAC = 角CBD

BC 平行于 FD,那么角BDF = 角CBD = 角A

终究只需求出角BDF就算出了角A

假定现在知道角A, AB的长度 = 小圆的半径

就能够算出:

  • BC = AB * sin(角A)
  • AC = AB * cos(角A)

现在已知BF 和 FD的间隔

角BDF = arctan(BF / FD)

那么现在就核算出了角A的视点

  • p1X = 小圆.x + 小圆半径 * sin(角A)

  • p1Y = 小圆.y – 小圆半径 * cos(角A)

P2

辅佐图1.8:

android 自定义View:仿QQ拖拽效果

现在要求出P2的方位,也便是角E的方位

  • 角E.x = 大圆.x + DG
  • 角E.y = 大圆.y + EG

角BDE = 90度;

角BDF + 角EDG = 90度

那么角E = 角BDF

P1刚刚核算了角BDF,仍是热的.

  • P2.x =大圆.x + DE * sin(角E)
  • P2.y = 大圆.y – DE * cos(角E)

P3

辅佐图1.9:

android 自定义View:仿QQ拖拽效果

P3便是角K的方位

  • 角K.x = 小圆.x – KH
  • 角K.y = 小圆.y – BH

角KBH + 角HBD = 90度

角BDF + 角HBD = 90度

所以角KBH + 角BDF

KH = BK * sin(角KBH)

BK = BK * cos(角KBH)

  • P3.x = 小圆.x – KH

  • P3.y = 小圆.y – BH

P4

辅佐图1.10:

android 自定义View:仿QQ拖拽效果

  • 角A.x = 大圆.x – CD

  • 角A.y = 大圆.y + AC

角A + 角ADC = 90度

角BDF + 角ADC = 90度

所以角A = 角BDF

CD = AD * sin(角A)

AC = AD * cos(角A)

  • P4.x = 大圆.x – CD
  • p4.y = 大圆.y – AC

控制点

控制点就选大圆与小圆的中点即可

控制点.x = (大圆.x – 小圆.x) / 2 + 小圆.x

控制点.y = (大圆.y – 小圆.y) / 2 + 小圆.y

来看看完好代码:

/*
* 作者:史大拿
* @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)
    }
    // 制作辅佐圆
    ...
}

辅佐图1.11:

android 自定义View:仿QQ拖拽效果

能够看出,根本作用现已到达了,可是这个大圆看着很大,总感觉有地方不和谐

那是由于这些参数都是我自己随意写的,到时分这些参数UI都会给你,必定没有这么随意..

能够先吧大圆半径缩小一点再看看作用怎么

辅佐图1.12:

android 自定义View:仿QQ拖拽效果

看着作用其实还能够.

拖动回弹

拖动回弹是指当拖动大圆时分,没有超出辅佐圆的规模, 此刻大圆还在辅佐圆规模内,

那么就需求将大圆回弹到小圆方位上.

那么必定是松手(ACTION_UP)事情的时分来处理:

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) }

此刻经过动画来改动bigPointF必定是不可取的,由于他是懒加载

所以要修正初始化代码为:

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
}

假设对为什么要在onSizeChanged中调用不明白的主张看一下View生命周期

调用:

override fun onTouchEvent(event: MotionEvent): Boolean {
    when (event.action) {
         ....
        MotionEvent.ACTION_UP -> {
            // 大圆是否在辅佐圆规模内
            if (bigPointF.contains(smallPointF, MAX_RADIUS)) {
                // 回弹
                bigAnimator().start()
            } else {
                // 爆破
            }
        }
    }
    invalidate()
    return true // 消费事情
}

辅佐图1.13:

android 自定义View:仿QQ拖拽效果

最后当大圆拖动到辅佐圆外的时分,在UP方位制作爆破作用,

而且当爆破作用完毕时分,吧大圆x,y坐标回到小圆坐标即可!

制作爆破作用

爆破作用其实便是20张图片一向在切换,到达一帧一帧的作用即可

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 // 消费事情
}

制作BitMap:

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)
    }
    // 制作辅佐圆
     ....
}

辅佐图1.14:

android 自定义View:仿QQ拖拽效果

虽然爆破作用制作出来了,可是看着爆破时分略微略微有一点颤动,

这是由于爆破作用的这20张图片是我在网上找的,一张一张切出来的.. 手艺欠好,可能有一点歪歪扭扭,可是不打紧,到时分UI都会给你的@.@

到此刻,作用一就完结了..

作用二

那么接下来继续完结作用二吧~:

回顾作用二

辅佐图2.1:

android 自定义View:仿QQ拖拽效果

赛前预备:

android 自定义View:仿QQ拖拽效果

要想拖动View,那就必须有一个View,那么就以这个Button来演示吧~

辅佐图2.2:

android 自定义View:仿QQ拖拽效果

思路剖析:

  1. 经过setOnTouchListener{} 能够实现对View的触摸事情监听
  2. ACTION_DOWN事情时分,将当时View躲藏,经过WindowManager增加一个拖拽的气泡View((便是上面写好的), 而且给气泡View初始化好方位
  3. ACTION_MOVE 事情中不断的更新大圆的方位
  4. ACTION_UP事情的时分,判别是否在辅佐圆内,然后进行回弹或许爆破. 而且将拖拽气泡从WindowManager总删去掉

需求注意的是,既然经过setOnTouchListener{} 监听了移动方位,那么在拖拽View中 onTouchEvent()中的代码全都要删掉

这仅仅总体思路,还有许多细节,那就慢慢剖析吧~

将dragView增加WindowManager上

辅佐图2.3:

android 自定义View:仿QQ拖拽效果

辅佐图2.4:

android 自定义View:仿QQ拖拽效果

能够看出,现在有2个问题

  • 初始化方位不对
  • 当拖动的时分,状态栏变成了黑色

初始化方位不对

初始化方位不对,需求有2个初始点

  • 小圆初始点: 小圆初始点既是当时view的中心点

  • 大圆初始点: 大圆初始点便是当时按下的方位

当时View的中心点需求获取当时window的肯定坐标方位:

// location[0] = x;
// location[1] = y;
val location = IntArray(2)
view.getLocationInWindow(location) // 获取当时窗口的肯定坐标

大圆方位为当时点击屏幕的肯定方位:

即为 event.rawX; even.rawY

调用:

#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
    )
  }
  ....
}

辅佐图2.5:

android 自定义View:仿QQ拖拽效果

能够看出,根本现已没问题了,可是点击的,显着有一点偏下,这是由于肯定方位不包括状体栏的高度

所以需求在减去状态栏的高度即可

// 获取状态栏高度
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)
 }

当拖动的时分,状态栏变成了黑色

状态栏变成黑色,阐明是LayoutParams 彻底占满了整个屏幕

那么只需求手动给他不包括状态栏的高度即可

 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 // 设置透明度
        )
    }

来看看WindowManager.LayoutParams的源码

辅佐图2.6:

android 自定义View:仿QQ拖拽效果

这儿我调用的是5个参数的,我也没研讨过这5个参数是干嘛用的

我只认识三个

  • 透明度

type 和 flags我都是设置的默认值.

无论怎么这么写是可行的,当时作用

辅佐图2.7:

android 自定义View:仿QQ拖拽效果

现在流程现已走了50%

复制drawView中BitMap,而且制作

这个标题我有必要解释一下,每一个控件内其实都是bitmap制作的

我想要的作用是当拖动的时分,拖动的是控件,而不是变成一个红色的小球

所以在拖动过程中,咱们要复制出drawView中bitmap图片,然后在从头制作一下

肉眼看就现已到达了作用,可是关于代码来说,本体现已躲藏了,仅仅留下了一个复制品来展现罢了

// 从View中获取bitMap
fun View.getBackgroundBitMap(): Bitmap = let {
    this.buildDrawingCache()
    this.drawingCache
}

在DOWN中设置bitMap图片

在UP的时分清空图片

#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()
}

这儿的bitMapWidth现在还不必,后边会用到.

辅佐图2.8:

android 自定义View:仿QQ拖拽效果

回弹作用

回弹代码现已写好了,只需求调用一下即可

# 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 {
      // 爆破作用
    }
}

辅佐图2.9:

android 自定义View:仿QQ拖拽效果

爆破作用

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
            }
        }
    }
}
android 自定义View:仿QQ拖拽效果

能够看出,根本作用现已完结了,可是还有一点,假设仔细看,

偶然状况下在大圆回到校园方位的时分,会闪烁一下

解决闪烁问题也很简单,只需求让他鄙人一帧的时分在进行躲藏或许显现操作即可

那么只需求调用View#postOnAnimation{}方法即可,吧辅佐圆去掉,看看现在的作用

android 自定义View:仿QQ拖拽效果

此刻的作用现已接近完美了~

假设现在换个大一点的控件来看看作用:

android 自定义View:仿QQ拖拽效果

能够看出,作用是没问题,可是爆破规模有一点小,我想控件有多宽,爆破规模就有多大

在上面更新View中BitMap图片的时分会传递BitMap的宽度,所以直接设置一下即可

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
}

getBitMap是一个加载BitMap的扩展方法

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)
}

再来看看作用:

android 自定义View:仿QQ拖拽效果

能够看出,爆照作用会跟随着图片的宽度来改动

可是爆破的时分会有白底,这彻底是由于我不会用ps,真的不会切图.. 就这么将就的看吧…

最后在RecyclerView中实战一下!

rv的代码就不看了,太简单了

android 自定义View:仿QQ拖拽效果

能够看出,作用仍是不对,犯错原因子view抢事情没抢过recyclerView,那么只需求在ACTION_DOWN中抢一下事情即可

view.setOnTouchListener { v, event ->
            when (event.action) {
  MotionEvent.ACTION_DOWN -> {
    // 和父容器抢焦点
    view.parent.requestDisallowInterceptTouchEvent(true)
  }
              ...
}

来看终究作用:

android 自定义View:仿QQ拖拽效果

完好代码

原创不易,您的点赞便是对我最大的支撑!

抢手文章:

  • View生命周期
  • android CoordinatorLayout 从源码到实战..
  • android NestedScrollView 从源码到实战..
  • android 图解 PhotoView,从‘百草园’到‘三味书屋’!
  • android 浅析RecyclerView收回复用机制及实战(仿探探作用)