按键截屏功能源码流程讲解

来源:互联网 发布:货代软件哪家好 编辑:程序博客网 时间:2024/05/21 22:50

Android4.0开始,系统自带了截屏功能,使用方法是按下音量下(VOLUME_DOWN)键+电源(Power)键。

以模块来划分的话,截图功能的代码会依次调用Policy,SystemUI,Surface相关的代码,具体流程如下流程图所示:

流程图

Policy(PhoneWindowManager.java):在此处完成Key的捕获,当VOLUME_DOWN和Power键被几乎同时按下后,向SystemUI发送Message开始截图。

SystemUI(TakeScreenshotService.java和GlobalScreenshot.java):收到来自Client端的截屏请求后,开始调用Surface的API截屏,并将截取到的图片通过WindowManager以浮动窗口的形式显示给用户查看。

Surface(Surface.java和android_view_Surface.cpp):Framework层的Surface.java只是提供一个native方法,实际实现在JNI处的android_view_Surface.cpp中的doScreenshot(…)方法。


Android源码中对组合键的捕获

Android源码中对按键的捕获位于文件PhoneWindowManager.java(mydroid\frameworks\base\policy\src\com\android\
internal\policy\impl)中,这个类处理所有的键盘输入事件,比如home键、返回键等。其中函数interceptKeyBeforeQueueing()会对常用的按键做特殊处理。

在Android系统中,由于我们的每一个Android界面都是一个Activity,而界面的显示都是通过Window对象实现的,每个Window对象实际上都是PhoneWindow的实例,而每个PhoneWindow对象都一个PhoneWindowManager对象,当我们在Activity界面执行按键操作的时候,在将按键的处理操作分发到App之前,首先会回调PhoneWindowManager中的dispatchUnhandledKey()方法,该方法主要用于执行当前App处理按键之前的操作。然后在dispatchUnhandledKey()方法体中会调用interceptFallback()方法,之后调用了interceptKeyBeforeQueueing()方法,通过阅读我们我们知道该方法主要实现了对截屏按键的处理流程,这样我们继续看一下interceptKeyBeforeQueueing()方法的处理:

    @Override    public int interceptKeyBeforeQueueing(KeyEvent event, int policyFlags) {        //首先判断当前系统是否已经boot完毕,若尚未启动完毕,则所有的按键操作都将失效,若启动完成,则执行后续的操作        if (!mSystemBooted) {            // If we have not yet booted, don't let key events do anything.            return 0;        }        ...        // Handle special keys.        switch (keyCode) {            //这里我们只是关注音量减少按键和电源按键组合的处理事件,即截屏            case KeyEvent.KEYCODE_VOLUME_DOWN:            case KeyEvent.KEYCODE_VOLUME_UP:            case KeyEvent.KEYCODE_VOLUME_MUTE: {                if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) {                    if (down) {                        //按下音量减少按键的时候回进入到:case KeyEvent.KEYCODE_VOLUME_MUTE分支并执行相应的逻                        //辑,然后同时判断用户是否按下了电源键,若同时按下了电源键,则执行下面                        if (interactive && !mScreenshotChordVolumeDownKeyTriggered                                && (event.getFlags() & KeyEvent.FLAG_FALLBACK) == 0) {                            mScreenshotChordVolumeDownKeyTriggered = true;                            mScreenshotChordVolumeDownKeyTime = event.getDownTime();//按下开始时间                            mScreenshotChordVolumeDownKeyConsumed = false;                            cancelPendingPowerKeyAction();                            interceptScreenshotChord();//系统准备开始执行截屏操作的开始                        }                    } else {                        mScreenshotChordVolumeDownKeyTriggered = false;                        cancelPendingScreenshotChordAction();                    }                }             ...                     case KeyEvent.KEYCODE_POWER: {                result &= ~ACTION_PASS_TO_USER;                isWakeKey = false; // wake-up will be handled separately                if (down) {                    interceptPowerKeyDown(event, interactive);                } else {                    interceptPowerKeyUp(event, interactive, canceled);                }                break;            }           ...           return result;    }

Android源码中调用屏幕截图的接口

可以发现这里首先判断当前系统是否已经boot完毕,按下音量减少按键的时候回进入到:case KeyEvent.KEYCODE_VOLUME_MUTE分支并执行相应的逻辑,然后同时判断用户是否按下了电源键,若同时按下了电源键,则执行interceptScreenshotChord()方法。其实按下电源键最终也会进入函数interceptScreenshotChord()中,那么接下来看看这个函数干了什么工作:

    private void interceptScreenshotChord() {        //用两个布尔变量判断是否同时按了音量下键和电源键后,再计算两个按键响应Down事件之间的时间差不超过150毫秒,        //也就认为是同时按了这两个键后,算是真正的捕获到屏幕截屏的组合键。        if (mScreenshotChordEnabled                && mScreenshotChordVolumeDownKeyTriggered && mScreenshotChordPowerKeyTriggered                && !mScreenshotChordVolumeUpKeyTriggered) {            final long now = SystemClock.uptimeMillis();// 从开机到现在的毫秒数(手机睡眠的时间不包括在内)            if (now <= mScreenshotChordVolumeDownKeyTime + SCREENSHOT_CHORD_DEBOUNCE_DELAY_MILLIS                    && now <= mScreenshotChordPowerKeyTime                    + SCREENSHOT_CHORD_DEBOUNCE_DELAY_MILLIS) {                mScreenshotChordVolumeDownKeyConsumed = true;                cancelPendingPowerKeyAction();                mHandler.postDelayed(mScreenshotRunnable, getScreenshotChordLongPressDelay());            }        }    }

在方法体中我们最终会执行发送一个延迟的异步消息,请求执行截屏的操作,而这里的延时时间,若当前输入框是打开状态,则延时时间为输入框关闭时间加上系统配置的按键超时时间,若当前输入框没有打开则直接是系统配置的按键超时处理时间。

发送了异步消息之后系统最终会被我们发送的Runnable对象的run方法执行,这样我们看一下Runnable类型的mScreenshotRunnable的run方法的实现:

    private final Runnable mScreenshotRunnable = new Runnable() {        @Override        public void run() {            takeScreenshot();        }    };

接着看函数takeScreenshot():

private void takeScreenshot() {        synchronized (mScreenshotLock) {            if (mScreenshotConnection != null) {                return;            }            //创建了一个TakeScreenshotService对象然后调用了bindServiceAsUser,            //这样就创建了TakeScreenshotService服务并在服务创建之后发送了一个异步消息            ComponentName cn = new ComponentName("com.android.systemui",                    "com.android.systemui.screenshot.TakeScreenshotService");            Intent intent = new Intent();            intent.setComponent(cn);            ServiceConnection conn = new ServiceConnection() {                @Override                public void onServiceConnected(ComponentName name, IBinder service) {                    synchronized (mScreenshotLock) {                        if (mScreenshotConnection != this) {                            return;                        }                        Messenger messenger = new Messenger(service);                        Message msg = Message.obtain(null, 1);                        final ServiceConnection myConn = this;                        Handler h = new Handler(mHandler.getLooper()) {                            @Override                            public void handleMessage(Message msg) {                                synchronized (mScreenshotLock) {                                    if (mScreenshotConnection == myConn) {                                        mContext.unbindService(mScreenshotConnection);                                        mScreenshotConnection = null;                                        mHandler.removeCallbacks(mScreenshotTimeout);                                    }                                }                            }                        };                        msg.replyTo = new Messenger(h);                        msg.arg1 = msg.arg2 = 0;                        if (mStatusBar != null && mStatusBar.isVisibleLw())                            msg.arg1 = 1;                        if (mNavigationBar != null && mNavigationBar.isVisibleLw())                            msg.arg2 = 1;                        try {                            messenger.send(msg);                        } catch (RemoteException e) {                        }                    }                }                @Override                public void onServiceDisconnected(ComponentName name) {}            };            if (mContext.bindServiceAsUser(                    intent, conn, Context.BIND_AUTO_CREATE, UserHandle.CURRENT)) {                mScreenshotConnection = conn;                mHandler.postDelayed(mScreenshotTimeout, 10000);            }        }    }

可以看到这个函数使用AIDL绑定了service服务到”com.android.systemui.screenshot.TakeScreenshotService”,注意在service连接成功时,对message的msg.arg1和msg.arg2两个参数的赋值。其中在mScreenshotTimeout中对服务service做了超时处理。接着我们找到实现这个服务service的类TakeScreenshotService类,看看其实现的流程:

public class TakeScreenshotService extends Service {    private static final String TAG = "TakeScreenshotService";    private static GlobalScreenshot mScreenshot;    private Handler mHandler = new Handler() {        @Override        public void handleMessage(Message msg) {            switch (msg.what) {                case 1:                    final Messenger callback = msg.replyTo;                    if (mScreenshot == null) {                        mScreenshot = new GlobalScreenshot(TakeScreenshotService.this);                    }                    mScreenshot.takeScreenshot(new Runnable() {                        @Override public void run() {                            Message reply = Message.obtain(null, 1);                            try {                                callback.send(reply);                            } catch (RemoteException e) {                            }                        }                    }, msg.arg1 > 0, msg.arg2 > 0);            }        }    };

可以发现在在TakeScreenshotService类的定义中有一个Handler成员变量,而我们在启动TakeScreentshowService的时候回发送一个异步消息,这样就会执行mHandler的handleMessage方法,然后在handleMessage方法中我们创建了一个GlobalScreenshow对象,然后执行了takeScreenshot方法,好吧,继续看一下takeScreentshot方法的执行逻辑。

    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        //执行截屏事件的具体操作,然后SurfaceControl.screenshot方法截屏之后返回的是一个Bitmap对象        mScreenBitmap = SurfaceControl.screenshot((int) dims[0], (int) dims[1]);        if (mScreenBitmap == null) {//判断是否截屏成功            notifyScreenshotError(mContext, mNotificationManager);//发送截屏失败的notification通知            finisher.run();            return;        }        //判断截屏的图像是否需要旋转        if (requiresRotation) {            // Rotate the screenshot to the current orientation            Bitmap ss = Bitmap.createBitmap(mDisplayMetrics.widthPixels,                    mDisplayMetrics.heightPixels, Bitmap.Config.ARGB_8888);            Canvas c = new Canvas(ss);            c.translate(ss.getWidth() / 2, ss.getHeight() / 2);            c.rotate(degrees);            c.translate(-dims[0] / 2, -dims[1] / 2);            c.drawBitmap(mScreenBitmap, 0, 0, null);            c.setBitmap(null);            // Recycle the previous bitmap            mScreenBitmap.recycle();            mScreenBitmap = ss;        }        // Optimizations        mScreenBitmap.setHasAlpha(false);        mScreenBitmap.prepareToDraw();        // Start the post-screenshot animation        //开始截屏的动画        startAnimation(finisher, mDisplayMetrics.widthPixels, mDisplayMetrics.heightPixels,                statusBarVisible, navBarVisible);    }

其实看到这里,我们算是真正看到截屏的操作了,具体的工作包括对屏幕大小、旋转角度的获取,然后调用Surface类的screenshot方法截屏保存到bitmap中,之后把这部分位图填充到一个画布上,最后再启动一个延迟的拍照动画效果。如果再往下探究screenshot方法,发现已经是一个native方法了:

    /**     * Like {@link #screenshot(int, int, int, int)} but includes all     * Surfaces in the screenshot.     *     * @hide     */    public static native Bitmap screenshot(int width, int height);

使用JNI技术调用底层的代码,如果再往下走,会发现映射这这个jni函数在文件android_view_Surface.cpp中,这个真的已经是底层c++语言了,统一调用的底层函数是doScreenshot()方法。


截屏声音控制

GlobalScreenshot.java下

private MediaActionSound mCameraSound;
// Setup the Camera shutter soundmCameraSound = new MediaActionSound();mCameraSound.load(MediaActionSound.SHUTTER_CLICK); //设置截屏的声音
// Play the shutter sound to notify that we've taken a screenshot.播放快门音以通知我们拍摄了屏幕截图mCameraSound.play(MediaActionSound.SHUTTER_CLICK);

截屏通知消息控制

GlobalScreenshot.java下
SaveImageInBackgroundTask()函数中有mNotificationManager.notify(nId, n);
onPostExecute()函数中有mNotificationManager.notify(mNotificationId, n);
控制截屏成功的通知消息

notifyScreenshotError()函数中有nManager.notify(SCREENSHOT_NOTIFICATION_ID, n);控制截屏出错的通知消息


总结

在PhoneWindowManager的dispatchUnhandledKey方法中处理App无法处理的按键事件,当然也包括音量减少键和电源按键的组合按键

通过一系列的调用启动TakeScreenshotService服务,并通过其执行截屏的操作。

具体的截屏代码是在native层实现的。

截屏操作时候,若截屏失败则直接发送截屏失败的notification通知。

截屏之后,若截屏成功,则先执行截屏的动画,并在动画效果执行完毕之后,发送截屏成功的notification的通知。


可以参考这篇文章里面的栗子,本文部分内容也参考此文

0 0