前言
主要介绍如何实现自定义的浮动导航。
需求:
- 实现返回键,可以在任意app内都生效。
- 实现Home键,可以在任意app内,点击之后都将直接到Launcher界面。
- 悬浮球可以拖拽,移动位置。
出现的问题:
- 浮动框点击区域过大,透明区域设置触摸穿透无效。
- 折叠动画展开时,view显示不全。
- 重复创建多次View。
实现过程
第一步
设置权限
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
6.0系统以上,需要申请动态权限
requestPermissions(Manifest.permission.SYSTEM_ALERT_WINDOW, CODE_PERMISSION);
第二步
创建Service对象,通过Service对象控制创建浮动球。
理由:1.service只会创建并启动一次,如果在Activity界面进行创建了。当你的app切换到后台,再切换前台后,有可能会被回收,造成重复在界面之中添加浮动球。
public class DemoService extends Service {
FloatBallView ballView;
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public void onCreate() {
super.onCreate();
WindowManager windowManager = (WindowManager) this.getSystemService(Context.WINDOW_SERVICE);
ballView = new FloatBallView(this, windowManager);
//加载浮动球对象
ballView.showOpenIcon();
}
@Override
public void onDestroy() {
//销毁浮动球对象
ballView.removeWindow();
super.onDestroy();
}
}
很简单的一个Service对象。
//可以在任意Activity界面调用该Service。因为不限制
startService(new Intent(this, DemoService.class));
第三步
重点的内容都在FloatBallView 里面了。我下面简单介绍下如何使用
public class FloatBallView extends FrameLayout implements View.OnClickListener {
private List<ImageView> imageButton= new ArrayList<>();
private boolean isMenuOpen = false;
//展开后的View的LayoutParams对象
private WindowManager.LayoutParams layoutParams;
//展开前的view的LayoutParams对象
private WindowManager.LayoutParams willParams;
private WindowManager windowManager;
//展开前的View 的布局,也就是说,浮动球在折叠时是一个layout,折叠后又是一个layout。
// 这样处理主要就是为了解决浮动球触摸区域过大而又无法点击穿透的问题。
private FrameLayout flWill;
private void initLayoutParams() {
layoutParams = new WindowManager.LayoutParams();
willParams = new WindowManager.LayoutParams();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
layoutParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
willParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
} else {
layoutParams.type = WindowManager.LayoutParams.TYPE_PHONE;
willParams.type = WindowManager.LayoutParams.TYPE_PHONE;
}
willParams.format = PixelFormat.RGBA_8888;
layoutParams.format = PixelFormat.RGBA_8888;
layoutParams.gravity = Gravity.START | Gravity.TOP;
willParams.gravity = Gravity.START | Gravity.TOP;
layoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL |
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE |
WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS;
willParams.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL |
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE |
WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH;
//设置展开后的布局的宽高
layoutParams.width = 200;
layoutParams.height = 200;
//展开前的浮动球宽高 就采用当前布局的实际大小。
willParams.width = WindowManager.LayoutParams.WRAP_CONTENT;
willParams.height = WindowManager.LayoutParams.WRAP_CONTENT;
willParams.x = spUtil.getFloatBallX();
willParams.y = spUtil.getFloatBallY();
}
public FloatBallView(Context context, WindowManager windowManager) {
super(context);
spUtil = SpUtil.getInstance();
this.windowManager = windowManager;
initLayoutParams();
initLayout(context);
}
private void initLayout(Context context) {
LayoutInflater inflate = LayoutInflater.from(context);
//这个布局文件就不发了, 主要就是一个FrameLayout 然后里面一个ImageView对象。imageView设置了宽高而已
flWill = (FrameLayout) inflate.inflate(R.layout.float_will_logo, null);
//下面我会贴出float_will_menu 的layout文件
inflate.inflate(R.layout.float_will_menu, this);
imageButton.add((ImageView) findViewById(R.id.im_1));
imageButton.add((ImageView) findViewById(R.id.im_2));
imageButton.add((ImageView) findViewById(R.id.im_3));
imageButton.add((ImageView) findViewById(R.id.im_4));
for (ImageView image: imageButton) {
image.setOnClickListener(this);
}
flWill.setOnTouchListener(new FloatingOnTouchListener(willParams, windowManager, this));
this.setOnTouchListener(new FloatingOnTouchListener(layoutParams, windowManager, this));
}
//显示菜单,点击第一个浮动球duix 时,判断是否要展开,如果是要展开的话,刷新展开后的对象的x,y对象,并更新到windowManager之中。
public boolean showMenu() {
boolean isreturn;
if (!isMenuOpen) {
//显示
layoutParams.x = willParams.x - layoutParams.width / 2 + 35;
layoutParams.y = willParams.y - layoutParams.height / 2;
windowManager.addView(this, layoutParams);
showOpenAnim();
isreturn = true;
} else {
//隐藏
showCloseAnim();
isreturn = false;
}
return isreturn;
}
@Override
public void onClick(View view) {
switch (view.getId()) {
case R.id.im_1:
sendKeyEvent(KeyEvent.KEYCODE_HOME);// 模拟HOME按键
showCloseAnim();
break;
case R.id.im_2:
sendKeyEvent(KeyEvent.KEYCODE_BACK); //模拟返回键
showCloseAnim();
break;
case R.id.im_3:
//如果有需求,可以添加自己的按钮 点击事件
showCloseAnim(); //关闭
break;
case R.id.im_4:
//如果有需求,可以添加自己的按钮 点击事件
showCloseAnim(); //关闭
break;
default:
showCloseAnim(); //关闭
break;
}
}
/**
模拟物理按键事件发送逻辑
**/
private void sendKeyEvent(final int mCode) {
new Thread() {
@Override
public void run() {
sendEvent(KeyEvent.ACTION_UP, mCode);
try {
sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
sendEvent(KeyEvent.ACTION_DOWN, mCode);
super.run();
}
//实现发送物理按键的 关键方法。这种写法不用依赖系统源码进行编译。
private void sendEvent(int action, int mCode) {
Instrumentation instrumentation = new Instrumentation();
instrumentation.sendKeySync(new KeyEvent(action, mCode));
}
}.start();
}
private void showOpenAnim() {
int dp = dip2px(45);
int size = imageButton.size();
ImageView imageBtn;
int s = 360 / size;
for (int i = 0; i < size; i++) {
AnimatorSet set = new AnimatorSet();
//标题1与x轴负方向角度为20°,标题2为100°,转换为弧度
double a = -Math.cos(s * Math.PI / 360 * i);
double b = -Math.sin(s * Math.PI / 360 * i);
double x = a * dp;
double y = b * dp;
imageBtn= imageButton.get(i);
set.playTogether(
ObjectAnimator.ofFloat(imageBtn, "translationX", (float) (x * 0.25), (float) x),
ObjectAnimator.ofFloat(imageBtn, "translationY", (float) (y * 0.25), (float) y)
, ObjectAnimator.ofFloat(imageBtn, "alpha", 0, 1).setDuration(2000)
);
set.setInterpolator(new BounceInterpolator());
set.setDuration(500).setStartDelay(100);
set.start();
set.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
}
@Override
public void onAnimationEnd(Animator animation) {
//菜单状态置打开
isMenuOpen = true;
}
@Override
public void onAnimationCancel(Animator animation) {
}
@Override
public void onAnimationRepeat(Animator animation) {
}
});
}
}
private int dip2px(int value) {
float density = getResources()
.getDisplayMetrics().density;
return (int) (density * value + 0.5f);
}
/**
* 关闭扇形菜单的属性动画,参数与打开时相反
动画效果都可以重新自己定义的。
*/
private void showCloseAnim() {
if (!isMenuOpen) {
return;
}
int dp = dip2px(45);
int size = imageButton.size();
//for循环来开始小图标的出现动画
ImageView imageBtn;
int s = 360 / size;
//for循环来开始小图标的出现动画
for (int i = 0; i < size; i++) {
imageBtn= imageButton.get(i);
AnimatorSet set = new AnimatorSet();
double a = -Math.cos(s * Math.PI / 360 * i);
double b = -Math.sin(s * Math.PI / 360 * i);
double x = a * dp;
double y = b * dp;
set.playTogether(
ObjectAnimator.ofFloat(imageBtn, "translationX", (float) x, (float) (x * 0.25)),
ObjectAnimator.ofFloat(imageBtn, "translationY", (float) y, (float) (y * 0.25)),
ObjectAnimator.ofFloat(imageBtn, "alpha", 1, 0).setDuration(2000)
);
set.setDuration(500);
set.start();
set.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
}
@Override
public void onAnimationEnd(Animator animation) {
//菜单状态置关闭
isMenuOpen = false;
windowManager.removeView(FloatBallView.this);
}
@Override
public void onAnimationCancel(Animator animation) {
}
@Override
public void onAnimationRepeat(Animator animation) {
}
});
}
}
/**
* 显示开始
*/
public void showOpenIcon() {
//移除本身
//添加View 到该区域
windowManager.addView(flWill, willParams);
}
/**
* 销毁自己
*/
public void removeWindow() {
windowManager.removeView(flWill);
windowManager.removeView(this);
windowManager = null;
}
private void setWillLayout(int x, int y) {
willParams.x = willParams.x + x;
willParams.y = willParams.y + y;
windowManager.updateViewLayout(flWill, willParams);
}
public static class FloatingOnTouchListener implements View.OnTouchListener {
private int lastX;
private int lastY;
private int startX;
private int startY;
private WindowManager.LayoutParams layoutParams;
private WindowManager mWindowManager;
private FloatBallView floatBallView;
private long tiem;
private boolean isDoubleClick = false;
public FloatingOnTouchListener(WindowManager.LayoutParams layoutParams,
WindowManager mWindowManager, FloatBallView floatBallView) {
this.layoutParams = layoutParams;
this.floatBallView = floatBallView;
this.mWindowManager = mWindowManager;
}
@Override
public boolean onTouch(View view, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
if (System.currentTimeMillis() - tiem < 500) {
isDoubleClick = true;
} else {
isDoubleClick = false;
}
//注意取X,Y 值时 不要event.getX(),event.getY()
//因为要提取当前View 基于整个屏幕的坐标,而不是当前View 自己的坐标 ,否则会造成一直无法拖动。
lastX = (int) event.getRawX();
lastY = (int) event.getRawY();
startX = lastX;
startY = lastY;
tiem = System.currentTimeMillis();
break;
case MotionEvent.ACTION_MOVE:
int nowX = (int) event.getRawX();
int nowY = (int) event.getRawY();
int movedX = nowX - lastX;
int movedY = nowY - lastY;
lastX = nowX;
lastY = nowY;
layoutParams.x = layoutParams.x + movedX;
layoutParams.y = layoutParams.y + movedY;
//获取当前手指移动的x和y,通过updateViewLayout方法将改变后的x和y设置给button
mWindowManager.updateViewLayout(view, layoutParams);
if (view.getId() != R.id.ll_will_btn) {
floatBallView.setWillLayout(movedX, movedY);
}
break;
case MotionEvent.ACTION_UP:
if (isDoubleClick) {
break;
}
if (view.getId() == R.id.ll_will_btn) {
if (isLongPressed(startX, startY, event.getRawX(), event.getRawY())) {
floatBallView.showMenu(); //调用点击
mWindowManager.updateViewLayout(view, layoutParams);
} else {
floatBallView.showCloseAnim();
mWindowManager.updateViewLayout(view, layoutParams);
}
SpUtil.getInstance().setFloatBallXY(layoutParams.x, layoutParams.y);//存储x和y坐标值
} else {
if (isLongPressed(startX, startY, event.getRawX(), event.getRawY())) {
floatBallView.showCloseAnim();
}
}
break;
case MotionEvent.ACTION_OUTSIDE:
//如果在外部区域点击时的逻辑处理
break;
default:
break;
}
//返回true则消费事件,返回false则传递事件,此处特殊处理是为了和点击事件区分
return true;
}
//区分是点击事件还是触摸移动事件
private boolean isLongPressed(float lastX, float lastY, float thisX,
float thisY) {
float offsetX = Math.abs(thisX - lastX);
float offsetY = Math.abs(thisY - lastY);
if (offsetX <= 10 && offsetY <= 10) {
return true;
}
return false;
}
}
}
下面贴出float_will_menu.xml布局
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/im_1"
android:layout_width="35dp"
android:layout_height="35dp"
android:layout_gravity="bottom|center_horizontal"
android:background="@drawable/oval"
android:padding="6dp"
android:scaleType="fitXY"
android:src="@drawable/ico_home"
android:alpha="0"/>
<ImageView
android:id="@+id/im_2"
android:layout_width="35dp"
android:layout_height="35dp"
android:layout_gravity="bottom|center_horizontal"
android:background="@drawable/oval"
android:padding="6dp"
android:scaleType="fitXY"
android:src="@drawable/ico_back"
android:alpha="0" />
<ImageView
android:id="@+id/im_3"
android:layout_width="35dp"
android:layout_height="35dp"
android:layout_gravity="bottom|center_horizontal"
android:background="@drawable/oval"
android:padding="6dp"
android:scaleType="fitXY"
android:alpha="0"
/>
<ImageView
android:id="@+id/im_4"
android:layout_width="35dp"
android:layout_height="35dp"
android:layout_gravity="bottom|center_horizontal"
android:background="@drawable/oval"
android:padding="6dp"
android:scaleType="fitXY"
android:alpha="0"
/>
</merge>
到这里 浮动框就设置完了。如果觉得有用,请留言支持。谢谢
评论区