如題,需要定制長按Power鍵彈出的Dialog,UI上的大致效果是:全屏,中間下拉按鈕“Swipe Down To Power Off”下拉關機,底部左右兩側“Reboot”,“Cancel”按鈕,分別是重啟,取消操作。並要求彈出Dialog的同時,背景漸變模糊,操作控件有相應動畫效果,執行相應操作有同步動畫,退出界面背景漸變至透明消失。設計效果醬紫:
具體控件動畫要求就不再詳述。主要兩件事:1、關機流程,更准確的說應該是對長按Power鍵的處理;2、定制Dialog。
1、長按Power鍵,PWM將捕獲這一事件
/frameworks/base/services/core/java/com/android/server/policy/PhoneWindowManager.java(基於MTK-M版本)
在“interceptKeyBeforeQueueing”方法中,主要看片段:
1 case KeyEvent.KEYCODE_POWER: { 2 result &= ~ACTION_PASS_TO_USER; 3 isWakeKey = false; // wake-up will be handled separately 4 if (down) { 5 interceptPowerKeyDown(event, interactive); 6 } else { 7 interceptPowerKeyUp(event, interactive, canceled); 8 } 9 break; 10 }
再看“interceptPowerKeyDown”方法,包含了對多種情形下對長按電源鍵時間的處理,例如靜默來電響鈴、屏幕截圖以及關閉電源等。 系統將根據電源鍵被按住的時間長短以及相關按鍵的使用情況來決定如何恰當地處理當前的用戶操作。看下面片段:
// If the power key has still not yet been handled, then detect short // press, long press, or multi press and decide what to do. mPowerKeyHandled = hungUp || mScreenshotChordVolumeDownKeyTriggered || mScreenshotChordVolumeUpKeyTriggered; if (!mPowerKeyHandled) { if (interactive) { // When interactive, we're already awake. // Wait for a long press or for the button to be released to decide what to do. if (hasLongPressOnPowerBehavior()) { Message msg = mHandler.obtainMessage(MSG_POWER_LONG_PRESS); msg.setAsynchronous(true); mHandler.sendMessageDelayed(msg, ViewConfiguration.get(mContext).getDeviceGlobalActionKeyTimeout()); } } else { wakeUpFromPowerKey(event.getDownTime()); if (mSupportLongPressPowerWhenNonInteractive && hasLongPressOnPowerBehavior()) { Message msg = mHandler.obtainMessage(MSG_POWER_LONG_PRESS); msg.setAsynchronous(true); mHandler.sendMessageDelayed(msg, ViewConfiguration.get(mContext).getDeviceGlobalActionKeyTimeout()); mBeganFromNonInteractive = true; } else { final int maxCount = getMaxMultiPressPowerCount(); if (maxCount <= 1) { mPowerKeyHandled = true; } else { mBeganFromNonInteractive = true; } } } }
跟蹤“MSG_POWER_LONG_PRESS”到“powerLongPress”方法:
private void powerLongPress() { final int behavior = getResolvedLongPressOnPowerBehavior(); switch (behavior) { case LONG_PRESS_POWER_NOTHING: break; case LONG_PRESS_POWER_GLOBAL_ACTIONS: mPowerKeyHandled = true; if (!performHapticFeedbackLw(null, HapticFeedbackConstants.LONG_PRESS, false)) { performAuditoryFeedbackForAccessibilityIfNeed(); } showGlobalActionsInternal(); break; case LONG_PRESS_POWER_SHUT_OFF: case LONG_PRESS_POWER_SHUT_OFF_NO_CONFIRM: mPowerKeyHandled = true; performHapticFeedbackLw(null, HapticFeedbackConstants.LONG_PRESS, false); sendCloseSystemWindows(SYSTEM_DIALOG_REASON_GLOBAL_ACTIONS); mWindowManagerFuncs.shutdown(behavior == LONG_PRESS_POWER_SHUT_OFF); break; } }
看case “LONG_PRESS_POWER_GLOBAL_ACTIONS”中的“showGlobalActionsInternal”方法:
void showGlobalActionsInternal() { sendCloseSystemWindows(SYSTEM_DIALOG_REASON_GLOBAL_ACTIONS); if (mGlobalActions == null) { mGlobalActions = new GlobalActions(mContext, mWindowManagerFuncs); } final boolean keyguardShowing = isKeyguardShowingAndNotOccluded(); mGlobalActions.showDialog(keyguardShowing, isDeviceProvisioned()); if (keyguardShowing) { // since it took two seconds of long press to bring this up, // poke the wake lock so they have some time to see the dialog. mPowerManager.userActivity(SystemClock.uptimeMillis(), false); } }
終於找到你“showDialog”
/frameworks/base/services/core/java/com/android/server/policy/GlobalActions.java
/** * Show the global actions dialog (creating if necessary) * @param keyguardShowing True if keyguard is showing */ public void showDialog(boolean keyguardShowing, boolean isDeviceProvisioned) { mKeyguardShowing = keyguardShowing; mDeviceProvisioned = isDeviceProvisioned; if (mDialog != null) { mDialog.dismiss(); mDialog = null; // Show delayed, so that the dismiss of the previous dialog completes mHandler.sendEmptyMessage(MESSAGE_SHOW); } else { handleShow(); } }
在方法“handleShow”中“createDialog”並show
private void handleShow() { awakenIfNecessary(); mDialog = createDialog(); prepareDialog(); // If we only have 1 item and it's a simple press action, just do this action. if (mAdapter.getCount() == 1 && mAdapter.getItem(0) instanceof SinglePressAction && !(mAdapter.getItem(0) instanceof LongPressAction)) { ((SinglePressAction) mAdapter.getItem(0)).onPress(); } else { WindowManager.LayoutParams attrs = mDialog.getWindow().getAttributes(); attrs.setTitle("GlobalActions"); mDialog.getWindow().setAttributes(attrs); mDialog.show(); mDialog.getWindow().getDecorView().setSystemUiVisibility(View.STATUS_BAR_DISABLE_EXPAND); } }
“createDialog”的代碼就不再貼了,就是create了一個GlobalActionsDialog,至於Android原生的這個dialog構造,感興趣的可以看看。這里完成需求就只需要替換掉這個dialog為自定義Dialog就ok了。
2、全屏Dialog,主要通過Style定義
<style name="power_dialog_style" > <item name="android:windowBackground">@android:color/transparent</item> <item name="android:windowFrame">@null</item> <item name="android:windowIsTranslucent">true</item> <item name="android:windowNoTitle">true</item> <item name="android:windowFullscreen">true</item> </style>
幾個重要的屬性:
<item name="android:windowIsFloating">true</item><!--是否浮現在activity之上--> <item name="android:windowFullscreen">true</item> <item name="android:windowIsTranslucent">false</item><!--半透明--> <item name="android:windowNoTitle">true</item><!--無標題--> <item name="android:windowBackground">@android:color/transparent</item><!--背景透明--> <item name="android:backgroundDimEnabled">true</item><!--灰度--> <item name="android:backgroundDimAmount">0.5</item> <item name="android:alpha">0.3</item>
背景高斯模糊,找到個簡單的
public class BlurBuilder { private static final float BITMAP_SCALE = 0.4f; private static final float BLUR_RADIUS = 7.5f; public static Bitmap blur(View v) { return blur(v.getContext(), getScreenshot(v)); } public static Bitmap blur(Context ctx, Bitmap image) { int width = Math.round(image.getWidth() * BITMAP_SCALE); int height = Math.round(image.getHeight() * BITMAP_SCALE); Bitmap inputBitmap = Bitmap.createScaledBitmap(image, width, height, false); Bitmap outputBitmap = Bitmap.createBitmap(inputBitmap); RenderScript rs = RenderScript.create(ctx); ScriptIntrinsicBlur theIntrinsic = ScriptIntrinsicBlur.create(rs, Element.U8_4(rs)); Allocation tmpIn = Allocation.createFromBitmap(rs, inputBitmap); Allocation tmpOut = Allocation.createFromBitmap(rs, outputBitmap); theIntrinsic.setRadius(BLUR_RADIUS); theIntrinsic.setInput(tmpIn); theIntrinsic.forEach(tmpOut); tmpOut.copyTo(outputBitmap); return outputBitmap; } private static Bitmap getScreenshot(View v) { Bitmap b = Bitmap.createBitmap(v.getWidth(), v.getHeight(), Bitmap.Config.ARGB_8888); Canvas c = new Canvas(b); v.draw(c); return b; } }
To apply this to a fragment, add the following to onCreateView
:
final Activity activity = getActivity(); final View content = activity.findViewById(android.R.id.content).getRootView(); if (content.getWidth() > 0) { Bitmap image = BlurBuilder.blur(content); window.setBackgroundDrawable(new BitmapDrawable(activity.getResources(), image)); } else { content.getViewTreeObserver().addOnGlobalLayoutListener(new OnGlobalLayoutListener() { @Override public void onGlobalLayout() { Bitmap image = BlurBuilder.blur(content); window.setBackgroundDrawable(new BitmapDrawable(activity.getResources(), image)); } }); }
經驗證,這個模糊效果太簡陋,可以更改控制模糊效果的兩個參數“BITMAP_SCALE”,“BLUR_RADIUS”,貌似后者代價太大,這里改的是scale,實測改為0.1f還ok的。
至於漸變模糊,也找了很多方法,這里參考網上一種思路,在dialog布局中內嵌一個MATCH_PARENT的ImageView用於放置模糊圖片,由於dialog本身透明,只要對模糊圖片進行透明度alpha的0~1動畫處理即可實現“漸變”,同理退出時alpha由1~0“漸變”消失。
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <ImageView android:id="@+id/power_iv_blur" android:layout_width="match_parent" android:layout_height="match_parent" android:contentDescription="@null"/> <!-- 主布局 --> </FrameLayout>
獲取RootView模糊處理后,將其設置為該ImageView的background,並對其Alpha進行動畫處理,即可實現“漸變”效果。但在移植到系統中后發現背景始終無法模糊漸變,
原來就沒有獲得Dialog下背景,或者說獲得的是透明的Dialog的背景,因為這個GloabalActionDialog是由PhoneWindowManager直接彈出的,提供的Context不同於一般Activity的Context,通過context.getWindow().getDecorView().findViewById(android.R.id.content).getRootView()獲取,如果context為當前Dialog,獲取的是透明背景,而PhoneWindowManager提供的mContext不能強轉Activity,否則直接crash。這樣獲取不到背景,只能另辟蹊徑了,想到了截屏。
/frameworks/base/packages/SystemUI/src/com/android/systemui/screenshot/GlobalScreenshot.java
/** * Takes a screenshot of the current display and shows an animation. */ void takeScreenshot(Runnable finisher, boolean statusBarVisible, boolean navBarVisible) { // We need to orient the screenshot correctly (and the Surface api seems to take screenshots // only in the natural orientation of the device :!) mDisplay.getRealMetrics(mDisplayMetrics); float[] dims = {mDisplayMetrics.widthPixels, mDisplayMetrics.heightPixels}; float degrees = getDegreesForRotation(mDisplay.getRotation()); boolean requiresRotation = (degrees > 0); if (requiresRotation) { // Get the dimensions of the device in its native orientation mDisplayMatrix.reset(); mDisplayMatrix.preRotate(-degrees); mDisplayMatrix.mapPoints(dims); dims[0] = Math.abs(dims[0]); dims[1] = Math.abs(dims[1]); } // Take the screenshot mScreenBitmap = SurfaceControl.screenshot((int) dims[0], (int) dims[1]); if (mScreenBitmap == null) { notifyScreenshotError(mContext, mNotificationManager); finisher.run(); return; } // 省略部分代碼 }
通過“SurfaceControl.screenshot”截取背景,終於實現了漸變模糊。剩下的就是根據需求來的View屬性動畫了,這個教程都很多的。還有下拉關機這個滑動操作,這里參考的是滑動解鎖實現的,具體看參考資料。滑動處理部分:
private void handleTouch() { mSwipeDownLLayout.setOnTouchListener(new View.OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { boolean handled = false; final int action = event.getActionMasked(); final float rawY = event.getRawY(); if (null == mVelocityTracker) { mVelocityTracker = VelocityTracker.obtain(); } mVelocityTracker.addMovement(event); switch (action) { case MotionEvent.ACTION_DOWN: case MotionEvent.ACTION_POINTER_DOWN: handleDown(rawY); handled = true; break; case MotionEvent.ACTION_MOVE: handleMove(rawY); handled = true; break; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_POINTER_UP: handleUp(); handled = true; break; case MotionEvent.ACTION_CANCEL: reStartVBreathAnimation(); handled = true; break; default: handled = false; break; } return handled; } }); mBtnCancel.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { startCancelAnimation(); } }); mBtnReboot.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { startRebootAnimation(); } }); } private void handleDown(float rawY) { mEventDownY = rawY; stopVBreathAnimation(); } private void handleMove(float rawY) { mSwipeDownHeight = rawY - mEventDownY + mSwipeStartY; mBottomDownHeight = rawY - mEventDownY + mBottomStartY; if (mSwipeDownHeight <= mSwipeStartY) { mSwipeDownHeight = mSwipeStartY; mBottomDownHeight = mBottomStartY; } mSwipeDownLLayout.setY(mSwipeDownHeight); mBottomRLayout.setY(mBottomDownHeight); } private void handleUp() { //1. if user swipe down some distance, shut down if (mSwipeDownHeight > MIN_DISTANCE_Y_TO_SWIPE_OFF) { swipeDownToShut(); } else if (velocityTrigShut()) { //2. if user swipe very fast, shut down } else { //otherwise reset the controls resetControls(); } } /** * another way to shut down, if user swipe very fast */ private boolean velocityTrigShut() { final VelocityTracker velocityTracker = mVelocityTracker; velocityTracker.computeCurrentVelocity(1000); int velocityY = (int) velocityTracker.getYVelocity(); if(velocityY > MIN_VELOCITY_Y_TO_SWIPE_OFF){ swipeDownToShut(); return true; } if (mVelocityTracker != null) { mVelocityTracker.recycle(); mVelocityTracker = null; } return false; }
主要功能就基本完成了,開始從Demo移到系統中。這其中有兩點:1、framework-res新增資源;2、內置需求要求字體:Aovel Sans。
1、一般新增文件都是private的,即SDK無關的,都要在類似“/frameworks/base/core/res/res/values/symbols.xml”的symbols文件中聲明:
<resources> <java-symbol type="drawable" name="power_off" /> <java-symbol type="drawable" name="power_reboot" /> <java-symbol type="drawable" name="power_cancel" /> <java-symbol type="layout" name="dialog_layout_power" /> <java-symbol type="id" name="power_layout" /> <java-symbol type="id" name="power_iv_blur" /> <java-symbol type="id" name="power_ll_swipe_down" /> <java-symbol type="id" name="power_tv_swipe_down_label" /> <java-symbol type="id" name="power_btn_swipe_down" /> <java-symbol type="id" name="power_rl_bottom" /> <java-symbol type="id" name="power_btn_reboot" /> <java-symbol type="id" name="power_tv_reboot_label" /> <java-symbol type="id" name="power_btn_cancel" /> <java-symbol type="id" name="power_tv_cancel_label" /> <java-symbol type="style" name="power_dialog_style" /> <java-symbol type="string" name="power_swipe_down_label" /> <java-symbol type="string" name="power_cancel_label" /> <java-symbol type="string" name="power_reboot_label" /> </resources>
然后單模塊編譯,先編譯“/frameworks/base/core/res/”,再編譯“/frameworks/base/”,然后再renew一編,否則可能會出現R文件錯誤。
2、內置字體於“/frameworks/base/data/fonts”,可以參照其他系統字體內置方式,最后會生成在“/system/fonts/”下,可以通過
Typeface typeface = Typeface.createFromFile("/system/fonts/AovelSans.ttf");
mTvSwipeDownLabel.setTypeface(typeface);
方式設置,也可以直接通過“android:fontFamily”方式設置:
<TextView android:id="@+id/power_tv_swipe_down_label" android:layout_width="wrap_content" android:layout_height="wrap_content" android:gravity="center" android:fontFamily="aovelsans" android:text="@string/power_swipe_down_label" android:textSize="24sp"/>
整個需求就完成了。還有待改善的是對背景色的判斷,當前慶幸在模糊淺色背景時,Dialog中控件及字體會看不清楚。可以在淺色背景時自動加深背景色避免。
參考資料: 深入解析Android關機
Android長按Power鍵彈出關機Dialog框GlobalActions解析