<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          厲害了!仿QQ拖拽效果

          共 36887字,需瀏覽 74分鐘

           ·

          2022-12-17 16:02

           安卓進(jìn)階漲薪訓(xùn)練營,讓一部分人先進(jìn)大廠


          大家好,我是皇叔,最近開了一個安卓進(jìn)階漲薪訓(xùn)練營,可以幫助大家突破技術(shù)&職場瓶頸,從而度過難關(guān),進(jìn)入心儀的公司。


          詳情見文章:沒錯!皇叔開了個訓(xùn)練營


          作者:史大拿
          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ǔ)上改的,可以通過一行代碼,讓所有控件都能實現(xiàn)拖拽效果!
          所以先來編寫效果一的代碼~

          基礎(chǔ)繪制

          首先編寫一下基礎(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

                  // 最大范圍(半徑),超出這個范圍大圓不顯示
                  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(2025500)
                  canvas.drawCircle(smallPointF.x, smallPointF.y, MAX_RADIUS, paint)
              }
          }
          這段代碼很簡單,都是一些基礎(chǔ)api的調(diào)用,輔助圓的作用:
          • 當(dāng)大圓超出輔助圓范圍的時候,大圓得“爆炸”,
          • 如果大圓未超出輔助圓內(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)事件
          }
          大圓動起來很簡單,只需要在ACTION_MOVE中一直刷新移動位置即可
          輔助圖1.1:
          我們想要的效果是手指按下之后,大圓跟著移動,
          輔助圖1.1后半段可以看出這里有一個小問題, 手指按什么位置小球就移動到什么位置,不是我們想要的效果
          那么我們知道所有的事件都是在DOWN中分發(fā)出來的,
          所以只需要在DOWN事件中判斷當(dāng)前是否點(diǎn)擊到大圓即可,
          // 標(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)事件
          }
          contains是自己寫的一個擴(kuò)展函數(shù):
          // 判斷一個點(diǎn)是否在另一個點(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
          }
          輔助圖1.2:
          大圓超出輔助圓范圍就消失
          有了PointF.contains() 這個擴(kuò)展,任務(wù)就變得輕松起來了
          只需要在繪制的時候判斷一下當(dāng)前位置即可
          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)
               }
                // 繪制小圓
               ...

              // 繪制輔助圓
              ...
          }
          輔助圖1.3:
          大圓越往外,小球越小
          要想求出大圓是否越往外,那么就得先計算出當(dāng)前大圓與小圓的距離
          輔助圖1.4:
          • 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中自帶的運(yùn)算符重載函數(shù)
          知道大圓和小圓的距離之后,就可以計算出比例
          比例 = 距離 / 總長度
          // 大圓與小圓之間的距離
          val d = distance()

          // 總長度
          var ratio = d / MAX_RADIUS
          // 如果當(dāng)前比例 > 0.618 那么就讓=0.618
          if (ratio > 0.618) {
              ratio = 0.618f
          }
          為什么要選0.618,
          0.618是黃金比例分割點(diǎn),聽說選了0.618繪制出來的東西會很協(xié)調(diào)?
          我一個糙人也看不出來美不美, 可能是看著更專業(yè)一點(diǎn)吧.
          完整繪制小圓代碼:
          //小圓半徑
          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:
          繪制貝塞爾曲線
          接下來只需要求出這4個點(diǎn)連接起來 , 看起來就像是把他們連接起來了
          然后在找到一個控制點(diǎn), 通過貝塞爾曲線讓他稍微彎曲即可
          輔助圖1.6:
          • P1

          輔助圖1.7:
          最終就是算出角A的坐標(biāo)即可
          目前已知
          • 角A.x = 小圓.x + BC;
          • 角A.y = 小圓.y - AC ;

          Tips: 因為角A的坐標(biāo)在小圓中心點(diǎn)上面, 在android坐標(biāo)系中 角A.y = 小圓.y - AC ;
          • 角C = 90度;
          • 角ABD = 90度

          角ABC + 角BAC = 90度; 角ABC +角CBD = 90度;
          所以角BAC = 角CBD
          BC 平行于 FD,那么角BDF = 角CBD = 角A
          最終只要求出角BDF就算出了角A
          假設(shè)現(xiàn)在知道角A, AB的長度 = 小圓的半徑
          就可以算出:
          • BC = AB * sin(角A)
          • AC = AB * cos(角A)

          現(xiàn)在已知BF 和 FD的距離
          角BDF = arctan(BF / FD)
          那么現(xiàn)在就計算出了角A的角度
          • p1X = 小圓.x + 小圓半徑 * sin(角A)
          • p1Y = 小圓.y - 小圓半徑 * cos(角A)

          • P2

          輔助圖1.8:
          現(xiàn)在要求出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:
          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:
          • 角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

          • 控制點(diǎn)

          控制點(diǎn)就選大圓與小圓的中點(diǎn)即可
          控制點(diǎn).x = (大圓.x - 小圓.x) / 2 + 小圓.x
          控制點(diǎn).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)

               // 控制點(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()) // 移動到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)
           }
          調(diào)用:
          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)
              }

              // 繪制輔助圓
              ...
          }
          輔助圖1.11:
          可以看出,基本效果已經(jīng)達(dá)到了,但是這個大圓看著很大,總感覺有地方不協(xié)調(diào)
          那是因為這些參數(shù)都是我自己隨便寫的,到時候這些參數(shù)UI都會給你,肯定沒有這么隨意…
          可以先吧大圓半徑縮小一點(diǎn)再看看效果如何
          輔助圖1.12:

          看著效果其實還可以.

          拖動回彈

          拖動回彈是指當(dāng)拖動大圓時候,沒有超出輔助圓的范圍, 此時大圓還在輔助圓范圍內(nèi),
          那么就需要將大圓回彈到小圓位置上.
          那么肯定是松手(ACTION_UP)事件的時候來處理:
          private fun bigAnimator(): ValueAnimator {
              return ObjectAnimator.ofObject(this"bigPointF", PointFEvaluator(),
                  PointF(width / 2f, height / 2f)).apply {
                  duration = 400
                  interpolator = OvershootInterpolator(3f) // 設(shè)置回彈迭代器
              }
          }
          常見插值器:
          • AccelerateDecelerateInterpolator 動畫從開始到結(jié)束,變化率是先加速后減速的過程。
          • AccelerateInterpolator 動畫從開始到結(jié)束,變化率是一個加速的過程。
          • AnticipateInterpolator 開始的時候向后,然后向前甩
          • AnticipateOvershootInterpolator 開始的時候向后,然后向前甩一定值后返回最后的值
          • BounceInterpolator 動畫結(jié)束的時候彈起
          • CycleInterpolator 動畫從開始到結(jié)束,變化率是循環(huán)給定次數(shù)的正弦曲線。
          • DecelerateInterpolator 動畫從開始到結(jié)束,變化率是一個減速的過程。
          • LinearInterpolator 以常量速率改變
          • OvershootInterpolator 結(jié)束時候向反方向甩某段距離

          插值器參考鏈接(https://cloud.tencent.com/developer/article/1488956)
          最開始初始化大圓位置為:
          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中調(diào)用不明白的建議看一下View生命周期
          調(diào)用:
          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)事件
          }
          輔助圖1.13:
          最后當(dāng)大圓拖動到輔助圓外的時候,在UP位置繪制爆炸效果,
          并且當(dāng)爆炸效果結(jié)束時候,吧大圓x,y坐標(biāo)回到小圓坐標(biāo)即可!

          爆炸效果

          爆炸效果其實就是20張圖片一直在切換,達(dá)到一幀一幀的效果即可
          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()
              }

          // 屬性動畫修改爆炸下標(biāo),最后一幀的時候回到 -1
          private val explodeAnimator by lazy {
                  ObjectAnimator.ofInt(this"explodeIndex"19-1).apply {
                      duration = 1000
                  }
              }
          調(diào)用:
          override fun onTouchEvent(event: MotionEvent)Boolean {
              when (event.action) {
                      ...
                  MotionEvent.ACTION_UP -> {
                      // 大圓是否在輔助圓范圍內(nèi)
                      if (bigPointF.contains(smallPointF, MAX_RADIUS)) {
                          // 回彈
                           ....
                      } else {
                          //  繪制爆炸效果
                          explodeAnimator.start()
                          // 爆炸效果結(jié)束后,將圖片移動到原始位置
                          explodeAnimator.doOnEnd {
                              bigPointF.x = width / 2f
                              bigPointF.y = height / 2f
                          }
                      }
                  }
              }
              invalidate()
              return true // 消費(fèi)事件
          }
          繪制BitMap:
          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)
              }

              // 繪制輔助圓
               ....
          }
          輔助圖1.14:
          雖然爆炸效果繪制出來了,但是看著爆炸時候稍微稍微有一點(diǎn)抖動,
          這是因為爆炸效果的這20張圖片是我在網(wǎng)上找的,一張一張切出來的… 手藝不好,可能有一點(diǎn)歪歪扭扭,但是不打緊,到時候UI都會給你的@.@
          到此時,效果一就完成了…

          效果二

          賽前準(zhǔn)備:
          要想拖動View,那就必須有一個View,那么就以這個Button來演示吧~
          思路分析:
          1. 通過setOnTouchListener{} 可以實現(xiàn)對View的觸摸事件監(jiān)聽
          2. 在ACTION_DOWN事件時候,將當(dāng)前View隱藏,通過WindowManager添加一個拖拽的氣泡View((就是上面寫好的), 并且給氣泡View初始化好位置
          3. 在ACTION_MOVE 事件中不斷的更新大圓的位置
          4. 在ACTION_UP事件的時候,判斷是否在輔助圓內(nèi),然后進(jìn)行回彈或者爆炸. 并且將拖拽氣泡從WindowManager總刪除掉

          需要注意的是,既然通過setOnTouchListener{} 監(jiān)聽了移動位置,那么在拖拽View中 onTouchEvent()中的代碼全都要刪掉
          這只是總體思路,還有很多細(xì)節(jié),那就慢慢分析吧~
          將dragView添加WindowManager上
          可以看出,現(xiàn)在有2個問題
          • 初始化位置不對
          • 當(dāng)拖動的時候,狀態(tài)欄變成了黑色

          初始化位置不對
          初始化位置不對,需要有2個初始點(diǎn)
          • 小圓初始點(diǎn): 小圓初始點(diǎn)既是當(dāng)前view的中心點(diǎn)
          • 大圓初始點(diǎn): 大圓初始點(diǎn)即是當(dāng)前按下的位置

          當(dāng)前View的中心點(diǎn)需要獲取當(dāng)前window的絕對坐標(biāo)位置:
          // location[0] = x;
          // location[1] = y;
          val location = IntArray(2)
          view.getLocationInWindow(location) // 獲取當(dāng)前窗口的絕對坐標(biāo)
          大圓位置為當(dāng)前點(diǎn)擊屏幕的絕對位置:
          即為 event.rawX; even.rawY
          調(diào)用:
          #BlogDragBubbleUtil.kt

          when (event.action) {
            MotionEvent.ACTION_DOWN -> {
                    val location = IntArray(2)
                view.getLocationInWindow(location) // 獲取當(dāng)前窗口的絕對坐標(biāo)
                dragView.initPointF(
                  location[0].toFloat() + view.width / 2,
                  location[1].toFloat()+ view.height / 2 ,
                  event.rawX,
                  event.rawY
              )
            }
            ....
          }
          可以看出,基本已經(jīng)沒問題了,但是點(diǎn)擊的,明顯有一點(diǎn)偏下,這是因為絕對位置不包含狀體欄的高度
          所以需要在減去狀態(tài)欄的高度即可
          // 獲取狀態(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)
           }
          當(dāng)拖動的時候,狀態(tài)欄變成了黑色
          狀態(tài)欄變成黑色,說明是LayoutParams 完全占滿了整個屏幕
          那么只需要手動給他不包含狀態(tài)欄的高度即可
           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è)置透明度
                  )
              }
          來看看WindowManager.LayoutParams的源碼
          這里我調(diào)用的是5個參數(shù)的,我也沒研究過這5個參數(shù)是干嘛用的
          我只認(rèn)識三個
          • 透明度

          type 和 flags我都是設(shè)置的默認(rèn)值.
          無論如何這么寫是可行的,當(dāng)前效果
          現(xiàn)在流程已經(jīng)走了50%
          復(fù)制drawView中BitMap,并且繪制
          這個標(biāo)題我有必要解釋一下,每一個控件內(nèi)其實都是bitmap繪制的
          我想要的效果是當(dāng)拖動的時候,拖動的是控件,而不是變成一個紅色的小球
          所以在拖動過程中,我們要復(fù)制出drawView中bitmap圖片,然后在重新繪制一下
          肉眼看就已經(jīng)達(dá)到了效果,但是對于代碼來說,本體已經(jīng)隱藏了,只是留下了一個復(fù)制品來展示而已
           // 從View中獲取bitMap
          fun View.getBackgroundBitMap(): Bitmap = let {
              this.buildDrawingCache()
              this.drawingCache
          }
          在DOWN中設(shè)置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(..)


                              // 設(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()
          }
          這里的bitMapWidth現(xiàn)在還不用,后面會用到.
          回彈效果
          回彈代碼已經(jīng)寫好了,只需要調(diào)用一下即可
          # 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() // 開啟動畫
                      doOnEnd {  // 結(jié)束動畫回調(diào)
                          windowManager.removeView(dragView)
                          view.visibility = View.VISIBLE
                      }
                  }
              }
          }
          可以看出,基本效果已經(jīng)完成了,但是還有一點(diǎn),如果仔細(xì)看,
          偶爾情況下在大圓回到校園位置的時候,會閃爍一下
          解決閃爍問題也很簡單,只需要讓他在下一幀的時候在進(jìn)行隱藏或者顯示操作即可
          那么只需要調(diào)用View#postOnAnimation{}方法即可,吧輔助圓去掉,看看現(xiàn)在的效果
          此時的效果已經(jīng)接近完美了~
          假如現(xiàn)在換個大一點(diǎn)的控件來看看效果:
          可以看出,效果是沒問題,但是爆炸范圍有一點(diǎn)小,我想控件有多寬,爆炸范圍就有多大
          在上面更新View中BitMap圖片的時候會傳遞BitMap的寬度,所以直接設(shè)置一下即可
          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
          }
          getBitMap是一個加載BitMap的擴(kuò)展方法
          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)
          }
          可以看出,爆照效果會跟隨著圖片的寬度來變化
          但是爆炸的時候會有白底,這完全是因為我不會用ps,真的不會切圖… 就這么將就的看吧…
          最后在RecyclerView中實戰(zhàn)一下!
          rv的代碼就不看了,太簡單了
          可以看出,效果還是不對,出錯原因子view搶事件沒搶過recyclerView,那么只需要在ACTION_DOWN中搶一下事件即可
          view.setOnTouchListener { v, event ->
                      when (event.action) {
            MotionEvent.ACTION_DOWN -> {
              // 和父容器搶焦點(diǎn)
              view.parent.requestDisallowInterceptTouchEvent(true)
            }
                        ...
          }
          最終效果達(dá)成。
          思路參考 :https://www.jianshu.com/p/9eb9c61e6c8b
          完整代碼:https://gitee.com/lanyangyangzzz/custom-view-project




          為了防止失聯(lián),歡迎關(guān)注我防備的小號


           

                         微信改了推送機(jī)制,真愛請星標(biāo)本公號??

          瀏覽 66
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  黄色在线免费 | 成人一区视频 | 操小逼视频 | 一级片免费观看 | 成人黄色一级A片 |