<kbd id="5sdj3"></kbd>
<th id="5sdj3"></th>

  • <dd id="5sdj3"><form id="5sdj3"></form></dd>
    <td id="5sdj3"><form id="5sdj3"><big id="5sdj3"></big></form></td><del id="5sdj3"></del>

  • <dd id="5sdj3"></dd>
    <dfn id="5sdj3"></dfn>
  • <th id="5sdj3"></th>
    <tfoot id="5sdj3"><menuitem id="5sdj3"></menuitem></tfoot>

  • <td id="5sdj3"><form id="5sdj3"><menu id="5sdj3"></menu></form></td>
  • <kbd id="5sdj3"><form id="5sdj3"></form></kbd>

    厲害了!仿QQ拖拽效果

    共 36887字,需瀏覽 74分鐘

     ·

    2022-12-17 16:02

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


    大家好,我是皇叔,最近開(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

    廢話不多說(shuō),先來(lái)看今天要完成的效果:


    圖二是在圖一的基礎(chǔ)上改的,可以通過(guò)一行代碼,讓所有控件都能實(shí)現(xiàn)拖拽效果!
    所以先來(lái)編寫(xiě)效果一的代碼~

    基礎(chǔ)繪制

    首先編寫(xiě)一下基礎(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(2025500)
            canvas.drawCircle(smallPointF.x, smallPointF.y, MAX_RADIUS, paint)
        }
    }
    這段代碼很簡(jiǎn)單,都是一些基礎(chǔ)api的調(diào)用,輔助圓的作用:
    • 當(dāng)大圓超出輔助圓范圍的時(shí)候,大圓得“爆炸”,
    • 如果大圓未超出輔助圓內(nèi)的話,大圓得回彈回去~

    主要就是起到這樣的作用.
    大圓動(dòng)起來(lá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)事件
    }
    大圓動(dòng)起來(lái)很簡(jiǎn)單,只需要在ACTION_MOVE中一直刷新移動(dòng)位置即可
    輔助圖1.1:
    我們想要的效果是手指按下之后,大圓跟著移動(dòng),
    輔助圖1.1后半段可以看出這里有一個(gè)小問(wèn)題, 手指按什么位置小球就移動(dòng)到什么位置,不是我們想要的效果
    那么我們知道所有的事件都是在DOWN中分發(fā)出來(lái)的,
    所以只需要在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是自己寫(xiě)的一個(gè)擴(kuò)展函數(shù):
    // 判斷一個(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
    }
    輔助圖1.2:
    大圓超出輔助圓范圍就消失
    有了PointF.contains() 這個(gè)擴(kuò)展,任務(wù)就變得輕松起來(lái)了
    只需要在繪制的時(shí)候判斷一下當(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:
    大圓越往外,小球越小
    要想求出大圓是否越往外,那么就得先計(jì)算出當(dāng)前大圓與小圓的距離
    輔助圖1.4:
    • dx = 大圓.x - 小圓.x
    • dy = 大圓.y - 小圓.y

    通過(guò)勾股定理就可以計(jì)算出他們之間的距離
    // 小圓與大圓之間的距離
    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ù)
    知道大圓和小圓的距離之后,就可以計(jì)算出比例
    比例 = 距離 / 總長(zhǎng)度
    // 大圓與小圓之間的距離
    val d = distance()

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

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

    Tips: 因?yàn)榻茿的坐標(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的長(zhǎng)度 = 小圓的半徑
    就可以算出:
    • BC = AB * sin(角A)
    • AC = AB * cos(角A)

    現(xiàn)在已知BF 和 FD的距離
    角BDF = arctan(BF / FD)
    那么現(xiàn)在就計(jì)算出了角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剛剛計(jì)算了角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
    來(lái)看看完整代碼:
    /*
    * 作者:史大拿
    * @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)
     }
    調(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á)到了,但是這個(gè)大圓看著很大,總感覺(jué)有地方不協(xié)調(diào)
    那是因?yàn)檫@些參數(shù)都是我自己隨便寫(xiě)的,到時(shí)候這些參數(shù)UI都會(huì)給你,肯定沒(méi)有這么隨意…
    可以先吧大圓半徑縮小一點(diǎn)再看看效果如何
    輔助圖1.12:

    看著效果其實(shí)還可以.

    拖動(dòng)回彈

    拖動(dòng)回彈是指當(dāng)拖動(dòng)大圓時(shí)候,沒(méi)有超出輔助圓的范圍, 此時(shí)大圓還在輔助圓范圍內(nèi),
    那么就需要將大圓回彈到小圓位置上.
    那么肯定是松手(ACTION_UP)事件的時(shí)候來(lái)處理:
    private fun bigAnimator(): ValueAnimator {
        return ObjectAnimator.ofObject(this"bigPointF", PointFEvaluator(),
            PointF(width / 2f, height / 2f)).apply {
            duration = 400
            interpolator = OvershootInterpolator(3f) // 設(shè)置回彈迭代器
        }
    }
    常見(jiàn)插值器:
    • 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í)候向反方向甩某段距離

    插值器參考鏈接(https://cloud.tencent.com/developer/article/1488956)
    最開(kāi)始初始化大圓位置為:
    private val bigPointF by lazy { PointF(width / 2f + 300, height / 2f) }
    此時(shí)通過(guò)動(dòng)畫(huà)來(lái)改變bigPointF肯定是不可取的,因?yàn)樗菓屑虞d
    所以要修改初始化代碼為:
    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
    }
    如果對(duì)為什么要在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)大圓拖動(dòng)到輔助圓外的時(shí)候,在UP位置繪制爆炸效果,
    并且當(dāng)爆炸效果結(jié)束時(shí)候,吧大圓x,y坐標(biāo)回到小圓坐標(biāo)即可!

    爆炸效果

    爆炸效果其實(shí)就是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()
        }

    // 屬性動(dòng)畫(huà)修改爆炸下標(biāo),最后一幀的時(shí)候回到 -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é)束后,將圖片移動(dòng)到原始位置
                    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:
    雖然爆炸效果繪制出來(lái)了,但是看著爆炸時(shí)候稍微稍微有一點(diǎn)抖動(dòng),
    這是因?yàn)楸ㄐЧ倪@20張圖片是我在網(wǎng)上找的,一張一張切出來(lái)的… 手藝不好,可能有一點(diǎn)歪歪扭扭,但是不打緊,到時(shí)候UI都會(huì)給你的@.@
    到此時(shí),效果一就完成了…

    效果二

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

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

    初始化位置不對(duì)
    初始化位置不對(duì),需要有2個(gè)初始點(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的絕對(duì)坐標(biāo)位置:
    // location[0] = x;
    // location[1] = y;
    val location = IntArray(2)
    view.getLocationInWindow(location) // 獲取當(dāng)前窗口的絕對(duì)坐標(biāo)
    大圓位置為當(dāng)前點(diǎn)擊屏幕的絕對(duì)位置:
    即為 event.rawX; even.rawY
    調(dià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
        )
      }
      ....
    }
    可以看出,基本已經(jīng)沒(méi)問(wèn)題了,但是點(diǎn)擊的,明顯有一點(diǎn)偏下,這是因?yàn)榻^對(duì)位置不包含狀體欄的高度
    所以需要在減去狀態(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)拖動(dòng)的時(shí)候,狀態(tài)欄變成了黑色
    狀態(tài)欄變成黑色,說(shuō)明是LayoutParams 完全占滿了整個(gè)屏幕
    那么只需要手動(dòng)給他不包含狀態(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è)置透明度
            )
        }
    來(lái)看看WindowManager.LayoutParams的源碼
    這里我調(diào)用的是5個(gè)參數(shù)的,我也沒(méi)研究過(guò)這5個(gè)參數(shù)是干嘛用的
    我只認(rèn)識(shí)三個(gè)
    • 透明度

    type 和 flags我都是設(shè)置的默認(rèn)值.
    無(wú)論如何這么寫(xiě)是可行的,當(dāng)前效果
    現(xiàn)在流程已經(jīng)走了50%
    復(fù)制drawView中BitMap,并且繪制
    這個(gè)標(biāo)題我有必要解釋一下,每一個(gè)控件內(nèi)其實(shí)都是bitmap繪制的
    我想要的效果是當(dāng)拖動(dòng)的時(shí)候,拖動(dòng)的是控件,而不是變成一個(gè)紅色的小球
    所以在拖動(dòng)過(guò)程中,我們要復(fù)制出drawView中bitmap圖片,然后在重新繪制一下
    肉眼看就已經(jīng)達(dá)到了效果,但是對(duì)于代碼來(lái)說(shuō),本體已經(jīng)隱藏了,只是留下了一個(gè)復(fù)制品來(lái)展示而已
     // 從View中獲取bitMap
    fun View.getBackgroundBitMap(): Bitmap = let {
        this.buildDrawingCache()
        this.drawingCache
    }
    在DOWN中設(shè)置bitMap圖片
    在UP的時(shí)候清空?qǐng)D片
    #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)在還不用,后面會(huì)用到.
    回彈效果
    回彈代碼已經(jīng)寫(xiě)好了,只需要調(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() // 開(kāi)啟動(dòng)畫(huà)
                doOnEnd {  // 結(jié)束動(dòng)畫(huà)回調(diào)
                    windowManager.removeView(dragView)
                    view.visibility = View.VISIBLE
                }
            }
        }
    }
    可以看出,基本效果已經(jīng)完成了,但是還有一點(diǎn),如果仔細(xì)看,
    偶爾情況下在大圓回到校園位置的時(shí)候,會(huì)閃爍一下
    解決閃爍問(wèn)題也很簡(jiǎn)單,只需要讓他在下一幀的時(shí)候在進(jìn)行隱藏或者顯示操作即可
    那么只需要調(diào)用View#postOnAnimation{}方法即可,吧輔助圓去掉,看看現(xiàn)在的效果
    此時(shí)的效果已經(jīng)接近完美了~
    假如現(xiàn)在換個(gè)大一點(diǎn)的控件來(lái)看看效果:
    可以看出,效果是沒(méi)問(wèn)題,但是爆炸范圍有一點(diǎn)小,我想控件有多寬,爆炸范圍就有多大
    在上面更新View中BitMap圖片的時(shí)候會(huì)傳遞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是一個(gè)加載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)
    }
    可以看出,爆照效果會(huì)跟隨著圖片的寬度來(lái)變化
    但是爆炸的時(shí)候會(huì)有白底,這完全是因?yàn)槲也粫?huì)用ps,真的不會(huì)切圖… 就這么將就的看吧…
    最后在RecyclerView中實(shí)戰(zhàn)一下!
    rv的代碼就不看了,太簡(jiǎn)單了
    可以看出,效果還是不對(duì),出錯(cuò)原因子view搶事件沒(méi)搶過(guò)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)注我防備的小號(hào)


     

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

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

    手機(jī)掃一掃分享

    分享
    舉報(bào)
    評(píng)論
    圖片
    表情
    推薦
    點(diǎn)贊
    評(píng)論
    收藏
    分享

    手機(jī)掃一掃分享

    分享
    舉報(bào)

    <kbd id="5sdj3"></kbd>
    <th id="5sdj3"></th>

  • <dd id="5sdj3"><form id="5sdj3"></form></dd>
    <td id="5sdj3"><form id="5sdj3"><big id="5sdj3"></big></form></td><del id="5sdj3"></del>

  • <dd id="5sdj3"></dd>
    <dfn id="5sdj3"></dfn>
  • <th id="5sdj3"></th>
    <tfoot id="5sdj3"><menuitem id="5sdj3"></menuitem></tfoot>

  • <td id="5sdj3"><form id="5sdj3"><menu id="5sdj3"></menu></form></td>
  • <kbd id="5sdj3"><form id="5sdj3"></form></kbd>
    国产精品久久久久毛片SUV | 狠狠干天天干 | 日韩黄色A片 | 人成视频在线免费观看 | 天堂俺去俺来也www久久婷婷 |