前言

之前写过屏蔽系统导航栏功能的文章,具体可看Android6.0 源码修改之屏蔽导航栏虚拟按键(Home和RecentAPP)/动态显示和隐藏NavigationBar

在某些特殊定制的版本中要求完全去掉导航栏,那么当用户点进一些系统自带的应用界面如设置、联系人等,就没法退出了,虽然可以在actionBar中添加back按钮,但总不能每一个app都去添加吧。所以灵机一动我们就给系统添加一个全屏可拖拽的浮窗按钮,点击的时候处理返回键的逻辑。它大概长这样(审美可能丑了点,你们可以自由发挥)



图1 最终效果图

思路分析

  1. 通过分析之前的NavigationBar代码,发现系统是通过WindowManager添加View的方式来实现,此处我们也可以模拟这种方法来添加
  2. 添加悬浮窗以后监听触摸事件,跟随手指移动重新修改view的layoutParam
  3. 松手后获取当前X坐标,小于屏幕width的一半则平移归位至屏幕左边
  4. 添加系统的返回按键功能

一、添加悬浮窗

private void showFloatingWindow() {
DisplayMetrics outMetrics = new DisplayMetrics();
mWindowManager.getDefaultDisplay().getMetrics(outMetrics);
screenWidth = outMetrics.widthPixels;
screenHeight = outMetrics.heightPixels; layoutParams = new WindowManager.LayoutParams();
layoutParams.type = WindowManager.LayoutParams.TYPE_PHONE;
layoutParams.format = PixelFormat.RGBA_8888;
layoutParams.gravity = Gravity.LEFT | Gravity.TOP;
layoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
layoutParams.width = 100;
layoutParams.height = 100;
layoutParams.x = 200;
layoutParams.y = 200; button = new ImageButton(mContext);
button.setBackground(mContext.getResources().getDrawable(R.drawable.fab_background));//系统通讯录里的蓝色圆形图标
button.setImageResource(R.drawable.ic_sysbar_back);//系统本身的back图标
mWindowManager.addView(button, layoutParams);
isShowFloatingView = true;
}

代码很简单,就是通过windowManager添加一个ImageButton,宽高都是100的,位置在屏幕左上角为原点的200,200。需要注意的是因为我们是在源码里添加,而且是M的版本,所以type为WindowManager.LayoutParams.TYPE_PHONE。如果是在普通的app里注意事项可参考这篇

二、添加触摸事件监听

button.setOnTouchListener(new FloatingOnTouchListener());

private class FloatingOnTouchListener implements View.OnTouchListener {
private int lastX;
private int lastY; @Override
public boolean onTouch(View view, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
isDrag = false;
lastX = (int) event.getRawX();
lastY = (int) event.getRawY();
break; case MotionEvent.ACTION_MOVE:
isDrag = true;
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);
break; case MotionEvent.ACTION_UP:
if (isDrag) {
log("lastX=" + lastX + " screenWidth=" + screenWidth);
//手指抬起时,判断是需要滑动到屏幕左边还是屏幕右边
if (lastX >= screenWidth / 2) {
setAnimation(view, lastX, screenWidth);
} else {
setAnimation(view, -lastX, 0);
}
}
break;
}
//返回true则消费事件,返回false则传递事件,此处特殊处理是为了和点击事件区分
return isDrag || view.onTouchEvent(event);
}

三、添加抬起滑动归位动画

private void setAnimation(final View view, int fromX, int toX) {
final ValueAnimator animator = ValueAnimator.ofInt(fromX, toX);
if (Math.abs(fromX) < screenWidth / 4 || fromX > screenWidth * 3 / 4)
animator.setDuration(300);
else
animator.setDuration(600); animator.setInterpolator(new LinearInterpolator());
animator.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {}
@Override
public void onAnimationEnd(Animator animation) {
log("onAnimationEnd=");
savePreValue(layoutParams.x, layoutParams.y);
}
@Override
public void onAnimationCancel(Animator animation) {}
@Override
public void onAnimationRepeat(Animator animation) {}
});
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
int current = (int) animator.getAnimatedValue();
log("current=" + current); layoutParams.x = Math.abs(current);
mWindowManager.updateViewLayout(view, layoutParams);
}
});
animator.start();
}
}

同样是通过改变button的x和y值来达到滑动效果,只不过我只需要x平移,y为0,需要斜着滑的你们可自由发挥,为了使滑动看上去平滑,给动画添加了一个线性插值器,设置滑动时间,监听返回插值进度,这样动态设置给button。为了保存button的最终位置,添加了一个动画完成监听,并将x和y写入到SharedPreferences中保存。

四、添加点击返回功能

通过打印日志分析,系统导航栏的返回按键,发现其原理是通过KeyButtonView的触摸事件发送一个KeyEvent事件给系统来实现返回功能

源码位置frameworks\base\packages\SystemUI\src\com\android\systemui\statusbar\policy\KeyButtonView.java

public boolean onTouchEvent(MotionEvent ev) {
final int action = ev.getAction();
int x, y;
if (action == MotionEvent.ACTION_DOWN) {
mGestureAborted = false;
}
if (mGestureAborted) {
return false;
} switch (action) {
case MotionEvent.ACTION_DOWN:
//按下的时间
mDownTime = SystemClock.uptimeMillis();
setPressed(true);
if (mCode != 0) {//按下事件
sendEvent(KeyEvent.ACTION_DOWN, 0, mDownTime);
} else {
// Provide the same haptic feedback that the system offers for virtual keys.
performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY);
}
removeCallbacks(mCheckLongPress);
postDelayed(mCheckLongPress, ViewConfiguration.getLongPressTimeout());
break;
case MotionEvent.ACTION_MOVE:
x = (int)ev.getX();
y = (int)ev.getY();
setPressed(x >= -mTouchSlop
&& x < getWidth() + mTouchSlop
&& y >= -mTouchSlop
&& y < getHeight() + mTouchSlop);
break;
case MotionEvent.ACTION_CANCEL:
setPressed(false);
if (mCode != 0) {
sendEvent(KeyEvent.ACTION_UP, KeyEvent.FLAG_CANCELED);
}
removeCallbacks(mCheckLongPress);
break;
case MotionEvent.ACTION_UP:
final boolean doIt = isPressed();
setPressed(false);
if (mCode != 0) {
if (doIt) {//抬起事件
sendEvent(KeyEvent.ACTION_UP, 0);
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
playSoundEffect(SoundEffectConstants.CLICK);
} else {
sendEvent(KeyEvent.ACTION_UP, KeyEvent.FLAG_CANCELED);
}
} else {
// no key code, just a regular ImageView
if (doIt) {
performClick();
}
}
removeCallbacks(mCheckLongPress);
break;
} return true;
} //以下为我们给button添加的点击事件
private void sendEvent(int action, int flags, long when) {
int mCode = 4;
Log.e(TAG, "mCode="+mCode + " flags="+flags);
final int repeatCount = (flags & KeyEvent.FLAG_LONG_PRESS) != 0 ? 1 : 0;
final KeyEvent ev = new KeyEvent(when - 100, when, action, mCode, repeatCount,
0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0,
flags | KeyEvent.FLAG_FROM_SYSTEM | KeyEvent.FLAG_VIRTUAL_HARD_KEY,
InputDevice.SOURCE_KEYBOARD);
InputManager.getInstance().injectInputEvent(ev,
InputManager.INJECT_INPUT_EVENT_MODE_ASYNC);
} button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.e(TAG,"click dragButton ...");
final long mDownTime = SystemClock.uptimeMillis();
//onBackPressed();
sendEvent(KeyEvent.ACTION_DOWN, 0, mDownTime); new Handler().postDelayed(new Runnable() {
@Override
public void run() {
sendEvent(KeyEvent.ACTION_UP, 0, SystemClock.uptimeMillis());
}
}, 300);
}
});

需要注意的地方,系统返回键对应的code为4,所以mCode=4,KeyButtonView的触摸事件包含按下和抬起,所以我们只需模拟发送按下和抬起事件,可以看到抬起事件加了300ms的延时发送,这是关键不然系统不会处理。

Android6.0 源码修改之 仿IOS添加全屏可拖拽浮窗返回按钮的更多相关文章

  1. Android6.0 源码修改之 Contacts应用

    一.Contacts应用的主界面和联系人详情界面增加顶部菜单添加退出按钮 通过Hierarchy View 工具可以发现 主界面对应的类为 PeopleActivity 联系人详情界面对应的类为 Qu ...

  2. Android6.0 源码修改之屏蔽系统短信功能和来电功能

    一.屏蔽系统短信功能 1.屏蔽所有短信 android 4.2 短信发送流程分析可参考这篇 戳这 源码位置 vendor\mediatek\proprietary\packages\apps\Mms\ ...

  3. Android6.0 源码修改之Settings音量调节界面增加通话音量调节

    前言 今天客户提了个需求,因为我们的设备在正常情况下无法调节通话音量,只有在打电话过程中,按物理音量加减键才能出现调节通话音量seekBar,很不方便,于是乎需求就来了.需要优化两个地方 1.在正常情 ...

  4. Android6.0 源码修改之屏蔽导航栏虚拟按键(Home和RecentAPP)/动态显示和隐藏NavigationBar

    场景分析, 为了完全实现沉浸式效果,在进入特定的app后可以将导航栏移除,当退出app后再次将导航栏恢复.(下面将采用发送广播的方式来移除和恢复导航栏) ps:不修改源码的情况下,简单的沉浸式效果实现 ...

  5. Android6.0 源码修改之Setting列表配置项动态添加和静态添加

    写在前面 最近客户有个需求,要求增加操作Setting列表配置项的功能,是不是一脸懵,没关系,一图胜千言,接下来就上图.诺,就是这么个意思.   原来的列表配置项     增加了单个配置项     增 ...

  6. 在Ubuntu Server14.04上编译Android6.0源码

    此前编译过Android4.4的源码,但是现在Android都到了7.0的版本,不禁让我感叹Google的步伐真心难跟上,趁这周周末时间比较充裕,于是在过去的24小时里,毅然花了9个小时编译了一把An ...

  7. Android6.0源码下载编译刷入真机

    编译环境是Ubuntu12.04.手机nexus 5,编译安卓6.0.1源码并烧录到真机. 源码用的是科大的镜像:http://mirrors.ustc.edu.cn/aosp-monthly/,下载 ...

  8. Ubuntu16.04下编译android6.0源码

    http://blog.csdn.net/cnliwy/article/details/52189349 作为一名合格的android开发人员,怎么能不会编译android源码呢!一定要来一次说编译就 ...

  9. Android6.0源码分析之录音功能(一)【转】

    本文转载自:http://blog.csdn.net/zrf1335348191/article/details/54949549 从现在开始一周时间研究录音,下周出来一个完整的博客,监督,激励!!! ...

随机推荐

  1. DX11 Without DirectX SDK--06 DirectXMath数学库

    回到 DirectX11--使用Windows SDK来进行开发 xnamath.h原本是位于DirectX SDK的一个数学库,但是现在Windows SDK包含的数学库已经抛弃掉原来的xnamat ...

  2. C Primer Plus 第9章 函数 编程练习

    复习题: 8. int choice(int a,int b,int c){ int max; max = a; if (b > max) max = b; if (c > max) ma ...

  3. dubbo-admin和dubbo-monitor的安装

    一.安装dubbo-admin 去这里 http://download.csdn.net/download/u013081610/10044744 下载dubbo-admin.war 部署dubbo- ...

  4. Go 自带的 http/server.go 的连接解析 与 如何结合 master-worker 并发模式,提高单机并发能力

    作者:林冠宏 / 指尖下的幽灵 掘金:https://juejin.im/user/587f0dfe128fe100570ce2d8 博客:http://www.cnblogs.com/linguan ...

  5. SSM-MyBatis-14:Mybatis中智能标签

    ------------吾亦无他,唯手熟尔,谦卑若愚,好学若饥------------- 谈论到智能,有什么要想的没有? 我下面放张图 相信都见过这个吧,你在之前没有学习过框架的时候怎么写的,动态sq ...

  6. VirtualBox报错:不能为虚拟电脑XXX打开一个新任务

    报错产生的背景 今天在这里下载了一个用于VirtualBox的Kali Linux虚拟机文件(使用VirtualBox可以直接打开使用,不用执行安装过程).但是将该文件导入到VirtualBox中之后 ...

  7. hackathon活动复盘

    复盘: hackathon技术创新型.理念创新落地型评委的一个评估点:在公司怎么落地的问题5分钟的demonstration,要进行测试,5分钟很快的:表达方式:common language,让人能 ...

  8. Can I use MyBatis to generate Dynamic SQL without executing it?

    Although MyBatis was designed to execute the query after it builds it, you can make use of it's conf ...

  9. Docker for windows on VMware

    工作环境 操作系统:Windows 10 Pro x64 Hyper-V:已卸载. VMware:已安装. Virtual Box:无安装. 前言 鉴于Hyper-V在Windows桌面系统下的&qu ...

  10. websocket(一)--握手

    最近在琢磨怎么实现服务端的消息推送,因为以前都是通过客户端请求来获取信息的,如果需要实时信息就得轮询,比如通过ajax不停的请求. websocket相当于对HTTP协议进行了升级,客户端和服务端通过 ...