前言 之前寫過屏蔽系統導航欄功能的文章,具體可看 "Android6.0 源碼修改之屏蔽導航欄虛擬按鍵(Home和RecentAPP)/動態顯示和隱藏NavigationBar" 在某些特殊定製的版本中要求完全去掉導航欄,那麼當用戶點進一些系統自帶的應用界面如設置、聯繫人等,就沒法退出了,雖然可以在 ...
前言
之前寫過屏蔽系統導航欄功能的文章,具體可看Android6.0 源碼修改之屏蔽導航欄虛擬按鍵(Home和RecentAPP)/動態顯示和隱藏NavigationBar
在某些特殊定製的版本中要求完全去掉導航欄,那麼當用戶點進一些系統自帶的應用界面如設置、聯繫人等,就沒法退出了,雖然可以在actionBar中添加back按鈕,但總不能每一個app都去添加吧。所以靈機一動我們就給系統添加一個全屏可拖拽的浮窗按鈕,點擊的時候處理返回鍵的邏輯。它大概長這樣(審美可能醜了點,你們可以自由發揮)
圖1 最終效果圖
思路分析
- 通過分析之前的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的延時發送,這是關鍵不然系統不會處理。