老规矩,先上图:
悬浮球大家都知道,无非就是一个按钮+N个子Item,会靠边,会变小...
我大概看过网上的一些实现,用的最多的可能就是应用加android.permission.SYSTEM_ALERT_WINDOW权限,然后windowManager.addView(悬浮个球)
多数手机上确实可以,我比着别人写的代码也写了一次,模拟器上运行也很好,然后到我的小米手机上,诶!诶!诶!我的球呢???
到哪儿去了现在我也没找到,各位知道怎么解决的话也请不吝赐教,指导指导!
既然它不出来,劳资这暴脾气也不会惯着它,卷起袖子换个姿势撸:用PopupWindow实现的应用内悬浮球!
首先,整体确定下我们要实现的功能:
1、显示一个球在页面上方
2、这个球可以点击
3、点击之后能展开子菜单
4、子菜单点击之后能执行相应的操作
5、球可以跟随手指移动
6、手指离开屏幕之后球自动靠边
7、靠边5秒内没有操作自动缩小
我们一个一个说:
- 新建一个PopupWindow,背景设置一张圆图片,在需要显示时调用showAtLocation(View parent, int gravity, int x, int y)方法。恩,就这么简单。
- 额,这个放到后面,跟6一起解决。
- 最开始我在纠结展开的子项是跟悬浮球放在一起,还是另外单独管理,最后还是选择了第二种方案。如果跟跟悬浮球放在一起,那么我要在同一个PopupWindow去处理悬浮球和子项的显示状态、业务逻辑,代码看起来很冗余。分开处理,只关注对象本身需要实现的功能和对外暴露的公共方法,这样就简单了。
子项布局很简单,在一个LinearLayout里面循环添加了几张图片作为子项按钮,然后设置给PopupWindow做布局,当然,你要用更优美的布局替换来实现自己的需求,这里只做示例。子项本身也是一个PopupWindow,所以不需要我们去写对应的显示和隐藏的方法,直接调用PopupWindow的showAtLocation(...)和dismiss()就可以了;子项按钮的点击事件需要我们自己来实现,这里我在子项里定义了一个接口,悬浮球继承接口,实现子项的点击,为什么要传到悬浮球里处理呢,主要是不想让子项暴露,对外只展示悬浮球,悬浮球对外处理所有的功能。 - 悬浮球需要做的是定义一个变量,控制子项的显示状态。注意,在调用子项的showAtLocation(...)方法的时候,要计算自己的位置,告诉子项,TMD,跟着劳资,别乱跑。
- 这里算是比较关键的实现了,我们在悬浮球构造方法里添加对触摸事件的监听setTouchInterceptor(OnTouchListener l),对各种手势进行处理,思路大概是这样的:手指移动的时候调用PopupWindow的update(...)方法,对悬浮球的位置要不断更新;手指抬起的时候要判断是靠左边近还是靠右边近,直接更改x的值,然后也是用update(...)方法让悬浮球靠边站(PS:update(...)是瞬时移动的,由于时间较短,肉眼看着也不算突兀,强迫症自行修改实现滑动),悬浮球靠边之后,hanler延时5s下一道圣旨,在handleMessage(...)方法里边又是调用update(...)方法,对悬浮球尺寸进行操作,实现变小。
- 手指离开屏幕之后球自动靠边的功能我们在上边已经讲了,在MotionEvent.ACTION_UP这种情况下可以处理。但是悬浮球的点击事件怎么办,在onTouch(...)我们返回了true,点击事件走不到onClick(...)了,那就另辟蹊径吧:在手指按下的时候我们记录了按下的位置,手指抬起的时候我们记录了抬起的位置,如果两次位置之间的差值小于touchSlop,那么我们认为这是一次点击事件!之后就可以很愉快的在if(Math.abs(dx)
- 好吧,这个在5里边也已经解释过了。
代码里注释非常非常详细了,这里就不费键盘了,后面会贴出代码和Github地址。 - 好吧,这个在5里边也已经解释过了。
好了,上面就是整个悬浮球的实现,利用PopupWindow,避开了所有的权限申请,尤其在这么多机型面前,申请权限还不一定能实现,操蛋!!
但是!!上面说了,现在我在做游戏行业相关工作,游戏整个页面其实只是一个Activity(多activity反正我公司没有,别抬杠),上面实现的悬浮球也只是针对单Activity的(其实合理使用单 activity 配合 fragment,页面跳转更流畅,管理更方便,参考新版知乎),细节方面大家自行扩展。
附:
Github地址:https://github.com/StormFeng/FloatView.git
FloatPopup.java
public class FloatPopup extends PopupWindow implements FloatPopupItem.OnItemClickListener {
//设置悬浮按钮尺寸
private int size = Util.dp2px(50);
private int screenWidth = Resources.getSystem().getDisplayMetrics().widthPixels;
private int screenHeight = Resources.getSystem().getDisplayMetrics().heightPixels;
//触发移动事件的最小距离
private int touchSlop = new ViewConfiguration().getScaledTouchSlop();
//记录当前手指实时位置
private float curX,curY;
//记录手指按下时的位置
private float lastX,lastY;
//设置当前悬浮按钮的显示位置
private float showX,showY;
//记录当前悬浮按钮显示状态
private boolean showMenu = false;
//记录当前悬浮按钮显示位置
private boolean showLeft = true;
private FloatPopupItem item;
private Activity context;
private OnClickListener onClickListener;
private Handler handler;
private Message message;
private static FloatPopup floatPopup;
public static FloatPopup getInstance(){
if(floatPopup==null){
floatPopup = new FloatPopup(Util.getContext());
}
return floatPopup;
}
public void show(){
if(!floatPopup.isShowing()){
floatPopup.showAtLocation(Util.getContext().getWindow().getDecorView(),
Gravity.NO_GRAVITY,0,0);
floatPopup.setOnClickListener((OnClickListener) Util.getContext());
}
}
@SuppressLint("HandlerLeak")
public FloatPopup(Context context) {
this.context = (Activity) context;
item = new FloatPopupItem(context);
item.setOnItemClickListener(this);
handler = new Handler(){
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
//接受到消息,说明用户在规定时间没有操作悬浮按钮,这个时候还要判断下子选项是否展开,
//选项没有展开,那么就让悬浮按钮变小,靠边站
if(!showMenu){
toSmallIcon(msg.arg1,msg.arg2);
}
}
};
message = handler.obtainMessage();
message.what = 0;
ImageView iv = new ImageView(context);
iv.setMinimumWidth(size);
iv.setMinimumHeight(size);
iv.setImageDrawable(context.getResources().getDrawable(R.mipmap.ic_launcher_round));
setContentView(iv);
setWidth(size);
setHeight(size);
setFocusable(false);
setBackgroundDrawable(new ColorDrawable(0x00000000));
setOutsideTouchable(false);
setTouchInterceptor(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
curX = event.getRawX();
curY = event.getRawY();
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
lastX = event.getRawX();
lastY = event.getRawY();
break;
case MotionEvent.ACTION_MOVE:
// float ddx = lastX - curX;
// float ddy = lastY - curY;
// if(Math.abs(ddx)screenHeight-size/2){
update((int)event.getRawX() - size/2,screenHeight-size);
}else{
/*常规情况。但是这里为什么要减去size/2呢(还有上边)?
我们设置的位置对于按钮来说是它的左上角,这里减去size/2只是为了让我们的参考点移
动到按钮的中心位置,另外,滑动的时候会消除掉一顿一顿的情况,不信你试试没有减掉
size/2时是什么样子*/
update((int)event.getRawX() - size/2,(int)event.getRawY()-size/2);
}
/*当开始移动的时候要判断下子项是否展开,如果展开,关闭之后再移动*/
if(item.isShowing()){
item.dismiss();
showMenu = !showMenu;
}
break;
case MotionEvent.ACTION_UP:
/*这里就很好理解了,当我们手指抬起时,如果抬起的位置靠近左边(curX
FloatPopupItem.java
public class FloatPopupItem extends PopupWindow implements View.OnClickListener {
public int width;
private int height = Util.dp2px(50);
private OnItemClickListener onItemClickListener;
/**
* 尼玛,这里这么简单就别写备注了,免得被人以为瞧不起拿刀砍
* @param context
*/
public FloatPopupItem(Context context) {
LinearLayout layout = new LinearLayout(context);
layout.setOrientation(LinearLayout.HORIZONTAL);
layout.setGravity(Gravity.CENTER_VERTICAL);
for (int i = 0; i < 3; i++) {
ImageView iv = new ImageView(context);
iv.setMinimumWidth(height);
iv.setMinimumHeight(height);
iv.setImageDrawable(context.getResources().getDrawable(R.mipmap.ic_launcher_round));
iv.setTag(i);
layout.addView(iv);
width+=height;
iv.setOnClickListener(this);
}
setContentView(layout);
setWidth(LinearLayout.LayoutParams.WRAP_CONTENT);
setHeight(LinearLayout.LayoutParams.WRAP_CONTENT);
setBackgroundDrawable(new ColorDrawable(0x00000000));
setOutsideTouchable(false);
}
interface OnItemClickListener{
void onItemClick(int i);
}
public void setOnItemClickListener(OnItemClickListener onItemClickListener) {
this.onItemClickListener = onItemClickListener;
}
@Override
public void onClick(View v) {
int tag = (int) v.getTag();
if(onItemClickListener!=null){
onItemClickListener.onItemClick(tag);
}
}
}
写到最后:
谁要敢赞赏,别怪我翻脸!哼!