前言
之前写过屏蔽系统导航栏功能的文章,具体可看Android6.0 源码修改之屏蔽导航栏虚拟按键(Home和RecentAPP)/动态显示和隐藏NavigationBar
在某些特殊定制的版本中要求完全去掉导航栏,那么当用户点进一些系统自带的应用界面如设置、联系人等,就没法退出了,虽然可以在actionBar中添加back按钮,但总不能每一个app都去添加吧。所以灵机一动我们就给系统添加一个全屏可拖拽的浮窗按钮,点击的时候处理返回键的逻辑。它大概长这样(审美可能丑了点,你们可以自由发挥)
思路分析
- 通过分析之前的NavigationBar代码,发现系统是通过WindowManager添加View的方式来实现,此处我们也可以模拟这种方法来添加
- 添加悬浮窗以后监听触摸事件,跟随手指移动重新修改view的layoutParam
- 松手后获取当前X坐标,小于屏幕width的一半则平移归位至屏幕左边
- 添加系统的返回按键功能
一、添加悬浮窗
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的延时发送,这是关键不然系统不会处理。