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