介绍
设置了一个悬浮窗,点击展开后,期望点击或者滑动空白处自动折叠回原本的位置。
效果图
如何实现
1、瞄瞄源码
源码是最好的老师,看看PopupWindow是如何实现的:
设置PopupWindow最后需要调用showAsDropDown展示,看看它的WindowManager.LayoutParams是怎么设置的。
public void showAsDropDown(View anchor, int xoff, int yoff, int gravity) {
... 省略
final WindowManager.LayoutParams p =
createPopupLayoutParams(anchor.getApplicationWindowToken());
... 省略
}
protected final WindowManager.LayoutParams createPopupLayoutParams(IBinder token) {
... 省略
p.flags = computeFlags(p.flags);
... 省略
}
private int computeFlags(int curFlags) {
curFlags &= ~(
WindowManager.LayoutParams.FLAG_IGNORE_CHEEK_PRESSES |
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE |
WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE |
WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH |
WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS |
WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM |
WindowManager.LayoutParams.FLAG_SPLIT_TOUCH);
... 省略
if (mOutsideTouchable) {
// 找到你了🥳
curFlags |= WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH;
}
... 省略
return curFlags;
}
这个WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH点进去看看。
翻译一下:如果你已经设置了FLAG_NOT_TOUCH_MODAL,那么你可以设置FLAG_WATCH_OUTSIDE_TOUCH这个flag。这样一个点击事件如果发生在你的窗口之外的范围,你就会接收到一个特殊的MotionEvent.ACTION_OUTSIDE。注意,你只会接收到点击事件的第一下,而之后的DOWN/MOVE/UP等手势全都不会接收到。
/** Window flag: if you have set {@link #FLAG_NOT_TOUCH_MODAL}, you
* can set this flag to receive a single special MotionEvent with
* the action
* {@link MotionEvent#ACTION_OUTSIDE MotionEvent.ACTION_OUTSIDE} for
* touches that occur outside of your window. Note that you will not
* receive the full down/move/up gesture, only the location of the
* first down as an ACTION_OUTSIDE.
*/
public static final int FLAG_WATCH_OUTSIDE_TOUCH = 0x00040000;
再搜索下MotionEvent.ACTION_OUTSIDE
@Override
public boolean onTouchEvent(MotionEvent event) {
final int x = (int) event.getX();
final int y = (int) event.getY();
if ((event.getAction() == MotionEvent.ACTION_DOWN)
&& ((x < 0) || (x >= getWidth()) || (y < 0) || (y >= getHeight()))) {
dismiss();
return true;
} else if (event.getAction() == MotionEvent.ACTION_OUTSIDE) {
dismiss(); // 666🤙🏻🤙🏻🤙🏻,在这里监听ACTION_OUTSIDE然后dismiss()
return true;
} else {
return super.onTouchEvent(event);
}
}
2、使用WindowManager向窗口添加自定义MusicPlayerAndroidView
设置WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH与width = WRAP_CONTENT
val windowManager = getSystemService(WINDOW_SERVICE) as WindowManager
val layoutParam = WindowManager.LayoutParams().apply {
type = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
} else {
WindowManager.LayoutParams.TYPE_PHONE
}
format = PixelFormat.RGBA_8888
flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL or
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or
WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH
width = WindowManager.LayoutParams.WRAP_CONTENT // 注意此处用WRAP_CONTENT
height = WindowManager.LayoutParams.WRAP_CONTENT
gravity = Gravity.START or Gravity.TOP
}
val floatRootView = MusicPlayerAndroidView(this) // 自定义的view
windowManager.addView(floatRootView, layoutParam)
3、重写dispatchTouchEvent
override fun dispatchTouchEvent(event: MotionEvent): Boolean {
when (event.action and MotionEvent.ACTION_MASK) {
MotionEvent.ACTION_OUTSIDE -> {
if (!isFold) // 如果不是折叠状态,调用折叠动画
fold()
}
}
return super.dispatchTouchEvent(event)
}
4、动画
动画需要动态改变view的宽度,不能直接改变windowParams.width这样右边的圆角不好处理,并且不断调用updateViewLayout()做动画很卡顿。调用更多次的onMeasure与onLayout
简单设置动画后,发现效果很卡顿。这是因为我们设置了windowParams.width = WRAP_CONTENT,view的宽度不断变化。
private val minWidth = 50f.dp.toInt()
private val maxWidth = 150f.dp.toInt()
private val extendAnimator by lazy {
ValueAnimator.ofInt(minWidth, maxWidth).apply {
addUpdateListener {
mBinding.root.layoutParams = mBinding.root.layoutParams.apply {
width = it.animatedValue as Int
}
}
}
}
private val foldAnimator by lazy {
ValueAnimator.ofInt(maxWidth, minWidth).apply {
addUpdateListener {
val value = it.animatedValue as Int
mBinding.root.layoutParams = mBinding.root.layoutParams.apply {
width = value
}
}
}
}
但是如果一开始就把windowParams的宽度设置为最大宽度呢?windowParams.width = 150f.dp.toInt()悬浮窗无法拖动到最右边。
所以我们需要在展开的时候,设置windowParams的宽度为动画最大宽度; 在折叠的时候,设置windowParams的宽度为动画最小宽度
windowManager.updateViewLayout(this, windowParams.apply {
width = maxWidth
})
extendAnimator.start()
private val foldAnimator by lazy {
ValueAnimator.ofInt(maxWidth, minWidth).apply {
addUpdateListener {
val value = it.animatedValue as Int
mBinding.root.layoutParams = mBinding.root.layoutParams.apply {
width = value
}
// 新加下面代码
if (value == minWidth) {
windowManager.updateViewLayout(this@MusicPlayerAndroidView, windowParams.apply {
width = minWidth
})
}
}
}
}