关于一条定制长按Power键弹出Dialog的需求

  如题,需要定制长按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解析

      Android全屏对话框(附带延时关闭效果)

      dialog style 的 activity 的全屏设置 

      弹出Dialog后背景模糊

      简单高斯模糊

      滑动模糊渐变效果

     Android 属性动画(Property Animation) 完全解析

      Android属性动画完全解析(上)

      Android属性动画深入分析

      Android高仿IOS7滑动解锁 

      Android滑动解锁实现

             Android字体工作原理与应用

             android字体工作原理

posted @ 2016-07-15 17:09  Droi  阅读(3547)  评论(0编辑  收藏  举报