模仿一個微信的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