文章目录
- 需求:系统实现悬浮窗菜单功能或悬浮小球定制功能
- 实际手机产品效果
- 悬浮窗作用
- 一、实际应用场景
- 二、应用上面实现功能
- 思路
- Demo演示效果
- 部分源码分析
- Service层
- View层
- View初始化
- view 添加到窗体
- 悬浮球拖动
- 重点代码:
- 三、系统上面实现功能
- 思路
- 系统服务SystemUIService
- 总结
需求:系统实现悬浮窗菜单功能或悬浮小球定制功能
- 模拟最早 iPhone4S 悬浮菜单功能,点击后显示菜单;当前苹果也有此功能
- 模拟当前部分Android 品类上面的悬浮球设置
实际手机产品效果
悬浮窗作用
- 关联手势控制:点击、双击、长按,控制关联功能定制
- 关联菜单:点击显示菜单,实现功能入口
一、实际应用场景
手机端已经把悬浮按钮功能实现很好了,但是都是隐藏的功能,手机自带手势功能已经非常友好了,有个悬浮窗反倒是影响了体验。但是对于部分其它带屏产品,悬浮窗功能还是有必要要的。
- 产品确实需要有一个简单悬浮菜单,屏蔽底部导航、保留或者屏蔽手势导航,一个菜单的入口
- 产品底部导航栏功能占用太多,放不下了,添加一个悬浮按钮,指定对应的功能
二、应用上面实现功能
首先在应用上面实现,后续移植,我们先实现悬浮按钮效果,对于控制管理在集成系统时需要考虑到架构相关,暂不考虑。
应用上其实就可以实现悬浮功能按钮,也方便管理,但是集成到系统里面,方便形成公版;应用端实现便于定制版本
思路
- 界面一定是在服务Service里面添加的,通过窗体Window 添加
- 那么窗体就是一个悬浮按钮
- 对悬浮按钮进行监听:点击、双击、长按、拖拽移动,实现具体的功能,如果实现菜单那其实就是展示另外一个窗体而已
Demo演示效果
功能演示:悬浮按钮,点击熄屏
悬浮圆点演示效果
部分源码分析
Service层
添加view,view 的初始化
View 的添加
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Log.d(TAG, "flags:" + flags + "__startId:" + startId);
String operate = null;
if (intent != null && !isBlank(operate = intent.getStringExtra("operate"))) {
if (equals(operate, "show")) {
mFloatView.addToWindow();
}
} else if (mOrientationLastIsShown && !mFloatView.isShown()) { // 小组件桌面显示的时候,旋转屏的时候会启动startCommand所以旋转屏的时候记录了状态
mFloatView.addToWindow();
}
mOrientationLastIsShown = true;// 清空旋转屏记录的状态
return super.onStartCommand(intent, flags, startId);
}
View 初始化,view 点击事件、长按事件
/**
* 初始化浮动小白点
*/
private void initFloatView() {
Log.d(TAG, "initFloatView: ");
WindowManager wm = (WindowManager) getSystemService(Context.WINDOW_SERVICE);
mScreenPoint = new Point();
wm.getDefaultDisplay().getSize(mScreenPoint);
if (mFloatView != null) {
mFloatView.removeFromWindow();
}
float x, y;
String lastpoint = SharePreferencesHelper.getInstance(mContext).get(FloatView.LAST_POINT_KEY);
if (!isBlank(lastpoint)) {
String point[] = lastpoint.split("\\*");
x = Float.valueOf(point[0]);
y = Float.valueOf(point[1]);
if (x != 0) {
x = mScreenPoint.x;
}
if (y > mScreenPoint.y) {
}
Log.d(TAG, "mScreenPoint.y:" + mScreenPoint.y + " mScreenPoint.x:" + mScreenPoint.x);
y = y * mScreenPoint.y / (mScreenPoint.x - 48);
} else { // 初始位置靠右边中间往下一些
x = mScreenPoint.x;
y = mScreenPoint.y * 3 / 4;
}
mFloatView = new FloatView(mContext, (int) x, (int) y, R.layout.float_layout);
mFloatView.setFloatViewClickListener(new FloatView.IFloatViewClick() {
@Override
public void onFloatViewClick() {
Log.d(TAG, "onFloatViewClick ");
}
});
mFloatView.setFloatViewLongClickListener(new FloatView.IFloatViewLongClick() {
@Override
public void onFloatViewLongClick() {
mFloatView.removeFromWindow();
Log.d(TAG," mFloatView onFloatViewLongClick");
}
});
}
View层
View初始化
private void initView(Context context, View childView, int x, int y) {
mContext = context;
mMaxMoveX = dip2px(mContext, 25);
mYOffset = dip2px(mContext, 15);
mMoveYOffset = dip2px(mContext, 15);
mMoveMinLimit = dip2px(mContext, 11);
floatIV = (ImageView) childView.findViewById(R.id.float_id);
wm = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE);
mScreenPoint = new Point();
wm.getDefaultDisplay().getSize(mScreenPoint);
wmParams = new WindowManager.LayoutParams();
wmParams.type = WindowManager.LayoutParams.TYPE_TOAST;
wmParams.gravity = Gravity.LEFT | Gravity.TOP;
wmParams.format = PixelFormat.RGBA_8888;
wmParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
wmParams.width = WindowManager.LayoutParams.WRAP_CONTENT;
wmParams.height = WindowManager.LayoutParams.WRAP_CONTENT;
wmParams.x = (int) x;
wmParams.y = (int) y;
wmParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
if (childView != null) {
addView(childView);
}
// 记录最后所在位置
SharePreferencesHelper.getInstance(mContext).set(LAST_POINT_KEY, x + "*" + y);
}
view 添加到窗体
/**
* 显示
*
* @return isAddtoWindow
*/
public boolean addToWindow() {
if (wm == null) {
return false;
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
if (isAttachedToWindow()) {
return false;
}
} else if (getParent() != null) {
return false;
}
if (!isShown()) {
if (floatIV != null) {
floatIV.setImageResource(mLastIVDrawable);
}
startPreparedSleep();
wm.addView(this, wmParams);
}
return true;
}
悬浮球拖动
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
if (event.getPointerCount() > 1)
return false;
isMove = false;
mHandler.removeMessages(SLEEP);
mHandler.sendEmptyMessageDelayed(LONG_CLICK, LONG_CLICK_TIME);
mLastIVDrawable = R.drawable.float_white_btn;
floatIV.setImageResource(R.drawable.float_white_big_btn);
mTouchStartX = (int) event.getRawX() - this.getMeasuredWidth() / 2;
mTouchStartY = (int) event.getRawY() - this.getMeasuredHeight() / 2 - mYOffset;
return true;
case MotionEvent.ACTION_MOVE:
if (!allowMove) {
return false;
}
int moveX = (int) event.getRawX() - this.getMeasuredWidth() / 2;
int moveY = (int) event.getRawY() - this.getMeasuredHeight() / 2 - mMoveYOffset;
if (Math.abs(moveY - mTouchStartY) > mMoveMinLimit || Math.abs(moveX - mTouchStartX) > mMoveMinLimit) { //移动位置较小时认为是没有移动
wmParams.x = moveX;
wmParams.y = moveY;
wm.updateViewLayout(this, wmParams);
mHandler.removeMessages(LONG_CLICK);
isMove = true;
}
return false;
case MotionEvent.ACTION_UP:
int x = (int) event.getRawX() - this.getMeasuredWidth() / 2;
int y = (int) event.getRawY() - this.getMeasuredHeight() / 2 - mYOffset;
startPreparedSleep();
mLastIVDrawable = R.drawable.float_white_btn;
floatIV.setImageResource(R.drawable.float_white_btn);
if (isMove) {
if (allowAutoMoveToSlide && allowMove) {
autoMoveSlide(x, y);
}
} else {
mHandler.removeMessages(LONG_CLICK);
/* if (listener != null) {
listener.onFloatViewClick();
}*/
Log.d(TAG," 点击了,处理点击事件");
goToSleep(mContext);
}
return true;
default:
break;
}
return false;
}
重点代码:
- wm.addView(this, wmParams); 添加操作
- wmParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; 窗体类型
三、系统上面实现功能
思路
- 参考应用功能实现,创建服务或者在系统服务里面实现功能。 个人认为放置到设置或者SystemUI,两者中我选择SystemUI
- 业务功能,比如View代码、资源代码放置到对应目录,能够引用即可
系统服务SystemUIService
Android12在线SystemUI源码
SystemUI顶层目录:
AndroidMenifest.xml 部分
<!-- Keep theme in sync with SystemUIApplication.onCreate().
Setting the theme on the application does not affect views inflated by services.
The application theme is set again from onCreate to take effect for those views. -->
<meta-data android:name="com.google.android.backup.api_key" android:value="AEdPqrEAAAAIWTZsUG100coeb3xbEoTWKd3ZL3R79JshRDZfYQ" />
<!-- Broadcast receiver that gets the broadcast at boot time and starts
up everything else.
TODO: Should have an android:permission attribute
-->
<service android:name="SystemUIService"
android:exported="true"
/>
SystemUIService 服务暂不分析,这个地方添加View即可
总结
源码参考:
系统悬浮框核心代码
应用端悬浮框源码
扩展:
菜单功能:只需要点击白点后添加一个wm View呀
控制功能:要么用SystemUI和Settings关联逻辑来控制;要么Service 通过binder 实现绑定,对外提供接口,通过aidl 来进程间通信控制