寫一個類似微信的懸浮框

模仿一個微信的Web 懸浮框

首先分析功能

1、懸浮框的點擊事件、長按事件、手勢拖拽,邊框吸附效果等等,當然了業務上還有添加多個item的效果,這個暫時先不處理

首先獲取權限

//獲取系統window 權限
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />

 

自定義一個view 

初始化窗口管理器

//窗口的配置參數
private var mLayoutParams: WindowManager.LayoutParams? = null
//獲取屏幕參數
private var mDisplayMetrics: DisplayMetrics? = null
//窗口管理
private var mWindowManager: WindowManager? = null
//右邊停靠位置
val RIGHT_POSTION = 1
//左邊停靠位置
val LEFT_POSTION = 2
//記錄當前懸浮位置 目前只支持左右位置
var DOCKING_POSITION = RIGHT_POSTION
/**
 * 初始化窗口管理器
 */
fun initWindowManager() {
    val dm = context.getResources().getDisplayMetrics()
    widthPixels = dm.widthPixels
    mWindowManager = context.applicationContext
            .getSystemService(Context.WINDOW_SERVICE) as WindowManager
    mDisplayMetrics = DisplayMetrics()
    mWindowManager!!.defaultDisplay.getMetrics(mDisplayMetrics)

}

 

  /**
     * 初始化WindowManager.LayoutParams參數
     */
    fun initLayoutParams() {
        if (mWindowManager == null) {
            mWindowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
        }
//        mWindowManager!!.removeView(this)
        mLayoutParams = WindowManager.LayoutParams()

        mLayoutParams!!.flags = (mLayoutParams!!.flags
                or WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH
                or WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL)
        mLayoutParams!!.dimAmount = 0.2f
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            mLayoutParams!!.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
        } else {
            mLayoutParams!!.type = WindowManager.LayoutParams.TYPE_SYSTEM_ALERT
        }
        mLayoutParams!!.height = 180
        mLayoutParams!!.width = 180//WindowManager.LayoutParams.WRAP_CONTENT
        mLayoutParams!!.gravity = Gravity.START or Gravity.TOP
        mLayoutParams!!.format = PixelFormat.RGBA_8888
        mLayoutParams!!.alpha = 1.0f //  設置整個窗口的透明度
        offsetX = 0F
        offsetY = getStatusBarHeight(context).toFloat()
        mLayoutParams!!.x = ((mDisplayMetrics!!.widthPixels - offsetX).toInt())
        mLayoutParams!!.y = ((mDisplayMetrics!!.heightPixels * 1.0f / 4 - offsetY).toInt())


        mWindowManager!!.addView(this, mLayoutParams)
    }

之後編寫數據更新

//更新位置
private fun updateViewLayout() {
    if (null != mLayoutParams) {

        if (mLayoutParams!!.x == 0) {
            Log.e("FloatManager", "顯示在最左邊 ${mLayoutParams!!.x}")
            DOCKING_POSITION = LEFT_POSTION
            isMoveing = false
        }
        if (mLayoutParams!!.x == widthPixels - width) {
            Log.e("FloatManager", "顯示在最右邊 ${mLayoutParams!!.x}")
            Log.e("FloatManager", "顯示在最右邊 ${widthPixels - width}")
            DOCKING_POSITION = RIGHT_POSTION
            isMoveing = false
        }

        invalidate()
        mWindowManager!!.updateViewLayout(this, mLayoutParams)
    }
}

初始化完畢處理 分析在什麼時候出發位置更新

監聽TouchEvent 事件

1、 處理用戶的click、LongClick 事件

2、處理用戶的拖拽事件

3、手指釋放後的邊框吸附效果

    /** 判斷是否有長按動作發生
     * @param lastX 按下時X座標
     * @param lastY 按下時Y座標
     * @param thisX 移動時X座標
     * @param thisY 移動時Y座標
     * @param lastDownTime 按下時間
     * @param thisEventTime 移動時間
     * @param longPressTime 判斷長按時間的閥值
     */
    fun isLongPressed(lastX: Int, lastY: Int,
                      thisX: Float, thisY: Float,
                      lastDownTime: Long, thisEventTime: Long,
                      longPressTime: Long): Boolean {
        var offsetX = Math.abs(thisX - lastX)
        var offsetY = Math.abs(thisY - lastY)
        var intervalTime = thisEventTime - lastDownTime
        if (offsetX <= 10 && offsetY <= 10 && intervalTime >= longPressTime) {
            return true
        }
        return false
    }


    /**
     * 觸摸事件
     */
    override fun onTouchEvent(event: MotionEvent?): Boolean {
        when (event!!.action) {
            MotionEvent.ACTION_DOWN -> {
                startX = event.x.toInt()
                startY = event.y.toInt()
                startTime = System.currentTimeMillis()
                isMoveing = false

                return false
            }
            MotionEvent.ACTION_MOVE -> {
                //當手指按住並移動時
                isMoveing = true
                mLayoutParams!!.x = (event.rawX - this!!.width / 2).toInt()
                mLayoutParams!!.y = (event.rawY - this!!.height).toInt()
                val curTime = System.currentTimeMillis()
                var isLongClick = isLongPressed(startX, startY, event.rawX, event.rawY, startTime, curTime, 300)
                if (isLongClick && (mLayoutParams!!.x - 20 < 0 || mLayoutParams!!.x > widthPixels - width - 20)) {
                    performLongClick()
                    return false

                } else {
                    Log.e("ACTION_MOVE ", "  x  :${mLayoutParams!!.x}")
                    Log.e("ACTION_MOVE ", "  y  :${mLayoutParams!!.y}")

                    updateViewLayout() //更新mView 的位置

                    return true
                }

            }
            MotionEvent.ACTION_UP -> {
                //當手指離開時
                val curTime = System.currentTimeMillis()
//                isMoveing = curTime - startTime > 100

                if (!isMoveing && curTime - startTime < 500 && curTime - startTime > 100) {
                    callOnClick()
                    return false
                }
                //判斷mView是在Window中的位置,以中間爲界
                finalMoveX = if (mLayoutParams!!.x + this!!.measuredWidth / 2 >= mWindowManager!!.defaultDisplay.width / 2) {
                    mWindowManager!!.defaultDisplay.width - this!!.measuredWidth
                } else {
                    0
                }
//處理邊框吸附效果,使用屬性動畫 讓吸附效果不會顯得比較突兀
                val animator = ValueAnimator.ofInt(mLayoutParams!!.x, finalMoveX).setDuration(Math.abs(mLayoutParams!!.x - finalMoveX).toLong())
                animator.addUpdateListener { animation: ValueAnimator ->
                    mLayoutParams!!.x = animation.animatedValue as Int
                    updateViewLayout()
                }
                animator.start()
                return isMoveing
            }
        }
        return false
    }

 

再然後處理view 展示效果

微信的樣式初始是一個橫向圓柱體 包裹一個圓、拖拽的時候變成一個圓球

那麼咱們需要做的就是 先繪製一個橫向圓柱體、在繪製一個圓、在拖拽的時候 在把圓柱體變成一個外圓 即可

吸附到邊框的時候根據左右狀態進行繪製不同方向的圓柱體

override fun onDraw(canvas: Canvas?) {
    super.onDraw(canvas)
    drawCircle(canvas)
}


/**
 * 繪製一個圓
 */
@SuppressLint("NewApi")
fun drawCircle(canvas: Canvas?) {

    if (isMoveing) {
        var paintYz = Paint()
        paintYz!!.setColor(Color.rgb(202, 202, 202))
        paintYz!!.setStrokeWidth(8F)
        paintYz!!.setStyle(Paint.Style.FILL)
        paintYz!!.alpha = 200
        paintYz!!.setShadowLayer(10f, 0f, 0f, Color.GRAY)
        canvas!!.drawCircle((width / 2).toFloat(), (height / 2).toFloat(), 70F, paintYz!!)
    }
    //判斷停靠哪個位置
    if (DOCKING_POSITION == LEFT_POSTION) {
        leftCylinder(canvas)
    } else {
        rightCylinder(canvas)
    }
    canvas!!.save()
//todo 此段代碼測試動態添加item 效果寫的測試,根據實際狀況來自己使用 
    if (list!!.size == 1) {
        canvas.drawCircle((width / 2).toFloat(), (height / 2).toFloat(), 50F, paint!!)
    } else if (list!!.size == 2) {
        paint!!.setColor(Color.RED)
        canvas.drawCircle(centerx - 20, centery, 50F / 5 * 3, paint!!)
     
        paint!!.setColor(Color.BLUE)
        canvas.drawCircle(centerx + 20, centery, 50F / 5 * 3, paint!!)

    } else
        canvas.drawCircle((width / 2).toFloat(), (height / 2).toFloat(), 50F, paint!!)
}


override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
    super.onSizeChanged(w, h, oldw, oldh)
    centerx = w / 2.toFloat()
    centery = h / 2.toFloat()
    wx = 20f
    wy = 20f

}

/**
 * 左邊圓柱的的樣式
 */
fun leftCylinder(canvas: Canvas?) {

    Log.e("FloatManager", "leftCylinder RectF  $wx    +  $wy")
    val oval = RectF(wx, wy,
            (width - wx), (height - wy))
    val jx = RectF(0f, wy - 1,
            (width / 2).toFloat(), (height - wy))

    if (!isMoveing) {
        //繪製半圓
        canvas!!.drawArc(oval, -90F, 180F, true, paintYz!!)
        //繪製矩形
        canvas!!.drawRect(jx, paintYz!!)

    }
}

/**
 * 右側的圓柱體
 */
fun rightCylinder(canvas: Canvas?) {

    Log.e("FloatManager", "rightCylinder RectF  $wx    +  $wy")
    val oval = RectF(wx, wy,
            ((width - wx).toFloat()), (height - wy))
    val jx = RectF((width / 2).toFloat(), wy - 1,
            ((width).toFloat()), (height - wy))

    if (!isMoveing) {
        //繪製半圓
        canvas!!.drawArc(oval, 90F, 180F, true, paintYz!!)
        //繪製矩形
        canvas!!.drawRect(jx, paintYz!!)

    }
}

好了大體代碼結構已經完畢,目前只是簡單的模仿效果,如果有朋友有比較特殊的想法可留言,共同提高

項目地址

https://github.com/wmyasw/KotlinMvpDemo/tree/master/floatwidget

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章