概述
本文基于Android5.1.1版本源码。
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(mydroidframeworksbasepolicysrccomandroid
internalpolicyimpl)中,这个类处理所有的键盘输入事件,比如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 sound
mCameraSound = 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的通知。
可以参考这篇文章里面的栗子,本文部分内容也参考此文
最后
以上就是留胡子滑板为你收集整理的按键截屏功能源码流程讲解Android源码中对组合键的捕获Android源码中调用屏幕截图的接口截屏声音控制截屏通知消息控制总结的全部内容,希望文章能够帮你解决按键截屏功能源码流程讲解Android源码中对组合键的捕获Android源码中调用屏幕截图的接口截屏声音控制截屏通知消息控制总结所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复