Android手機一般都自帶有手機屏幕截圖的功能:在手機任何界面(當然手機要是開機點亮狀態(tài)),通過按組合鍵,屏幕閃一下,然后咔嚓一聲,截圖的照片會保存到當前手機的圖庫中,真是一個不錯的功能!
以我手頭的測試手機為例,是同時按電源鍵+音量下鍵來實現(xiàn)截屏,蘋果手機則是電源鍵 + HOME鍵,小米手機是菜單鍵+音量下鍵,而HTC一般是按住電源鍵再按左下角的“主頁”鍵。那么Android源碼中使用組合鍵是如何實現(xiàn)屏幕截圖功能呢?前段時間由于工作的原因仔細看了一下,這兩天不忙,便把相關的知識點串聯(lián)起來整理一下,分下面兩部分簡單分析下實現(xiàn)流程:
Android源碼中對組合鍵的捕獲。
Android源碼中對按鍵的捕獲位于文件PhoneWindowManager.java(alps\frameworks\base\policy\src\com\android\internal\policy\impl)中,這個類處理所有的鍵盤輸入事件,其中函數(shù)interceptKeyBeforeQueueing()會對常用的按鍵做特殊處理。以我手頭的測試機為例,是同時按電源鍵和音量下鍵來截屏,那么在這個函數(shù)中我們會看到這么兩段代碼:
1 2 3 4 5 6 7 8 9101112131415161718192021222324252627282930 | ....... case KeyEvent.KEYCODE_VOLUME_DOWN: case KeyEvent.KEYCODE_VOLUME_UP: case KeyEvent.KEYCODE_VOLUME_MUTE: { if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) { if (down) { if (isScreenOn && !mVolumeDownKeyTriggered && (event.getFlags() & KeyEvent.FLAG_FALLBACK) == 0) { mVolumeDownKeyTriggered = true; mVolumeDownKeyTime = event.getDownTime(); mVolumeDownKeyConsumedByScreenshotChord = false; cancelPendingPowerKeyAction(); interceptScreenshotChord(); } } else { mVolumeDownKeyTriggered = false; cancelPendingScreenshotChordAction(); }...... case KeyEvent.KEYCODE_POWER: { result &= ~ACTION_PASS_TO_USER; if (down) { if (isScreenOn && !mPowerKeyTriggered && (event.getFlags() & KeyEvent.FLAG_FALLBACK) == 0) { mPowerKeyTriggered = true; mPowerKeyTime = event.getDownTime(); interceptScreenshotChord(); }...... |
可以看到正是在這里(響應Down事件)捕獲是否按了音量下鍵和電源鍵的,而且兩個地方都會進入函數(shù)interceptScreenshotChord()中,那么接下來看看這個函數(shù)干了什么工作:
1 2 3 4 5 6 7 8 910111213 | private void interceptScreenshotChord() { if (mVolumeDownKeyTriggered && mPowerKeyTriggered && !mVolumeUpKeyTriggered) { final long now = SystemClock.uptimeMillis(); if (now <= mVolumeDownKeyTime + SCREENSHOT_CHORD_DEBOUNCE_DELAY_MILLIS && now <= mPowerKeyTime + SCREENSHOT_CHORD_DEBOUNCE_DELAY_MILLIS) { mVolumeDownKeyConsumedByScreenshotChord = true; cancelPendingPowerKeyAction(); mHandler.postDelayed(mScreenshotChordLongPress, ViewConfiguration.getGlobalActionKeyTimeout()); } } } |
在這個函數(shù)中,用兩個布爾變量判斷是否同時按了音量下鍵和電源鍵后,再計算兩個按鍵響應Down事件之間的時間差不超過150毫秒,也就認為是同時按了這兩個鍵后,算是真正的捕獲到屏幕截屏的組合鍵。
附言:文件PhoneWindowManager.java類是攔截鍵盤消息的處理類,在此類中還有對home鍵、返回鍵等好多按鍵的處理。
Android源碼中調用屏幕截圖的接口。
捕獲到組合鍵后,我們再看看android源碼中是如何調用屏幕截圖的函數(shù)接口。在上面的函數(shù)interceptScreenshotChord中我們看到用handler判斷長按組合鍵500毫秒之后,會進入如下函數(shù):
12345 | private final Runnable mScreenshotChordLongPress = new Runnable() { public void run() { takeScreenshot(); } }; |
在這里啟動了一個線程來完成截屏的功能,接著看函數(shù)takeScreenshot():
1 2 3 4 5 6 7 8 910111213141516171819202122232425262728293031323334353637383940414243444546474849505152 | private void takeScreenshot() { synchronized (mScreenshotLock) { if (mScreenshotConnection != null) { return; } 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.bindService(intent, conn, Context.BIND_AUTO_CREATE)) { mScreenshotConnection = conn; mHandler.postDelayed(mScreenshotTimeout, 10000); } } } |
可以看到這個函數(shù)使用AIDL綁定了service服務到"com.android.systemui.screenshot.TakeScreenshotService",注意在service連接成功時,對message的msg.arg1和msg.arg2兩個參數(shù)的賦值。其中在mScreenshotTimeout中對服務service做了超時處理。接著我們找到實現(xiàn)這個服務service的類TakeScreenshotService,看看其實現(xiàn)的流程:
1 2 3 4 5 6 7 8 91011121314151617181920212223242526272829303132 | 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); } } }; @Override public IBinder onBind(Intent intent) { return new Messenger(mHandler).getBinder(); }} |
在這個類中,我們主要看調用接口,用到了mScreenshot.takeScreenshot()傳遞了三個參數(shù),第一個是個runnable,第二和第三個是之前message傳遞的兩個參數(shù)msg.arg1和msg.arg2。最后我們看看這個函數(shù)takeScreenshot(),位于文件GlobalScreenshot.java中(跟之前的函數(shù)重名但是文件路徑不一樣):
1 2 3 4 5 6 7 8 9101112131415161718192021222324252627282930313233343536373839404142434445464748 | /** * 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 = Surface.screenshot((int) dims[0], (int) dims[1]); if (mScreenBitmap == null) { notifyScreenshotError(mContext, mNotificationManager); 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); 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方法,發(fā)現(xiàn)已經(jīng)是一個native方法了:
1234567 | /** * 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技術調用底層的代碼,如果再往下走,會發(fā)現(xiàn)映射這這個jni函數(shù)在文件android_view_Surface.cpp中,這個真的已經(jīng)是底層c++語言了,統(tǒng)一調用的底層函數(shù)是:
1 2 3 4 5 6 7 8 91011121314151617181920212223242526272829303132 | static jobject doScreenshot(JNIEnv* env, jobject clazz, jint width, jint height, jint minLayer, jint maxLayer, bool allLayers){ ScreenshotPixelRef* pixels = new ScreenshotPixelRef(NULL); if (pixels->update(width, height, minLayer, maxLayer, allLayers) != NO_ERROR) { delete pixels; return 0; } uint32_t w = pixels->getWidth(); uint32_t h = pixels->getHeight(); uint32_t s = pixels->getStride(); uint32_t f = pixels->getFormat(); ssize_t bpr = s * android::bytesPerPixel(f); SkBitmap* bitmap = new SkBitmap(); bitmap->setConfig(convertPixelFormat(f), w, h, bpr); if (f == PIXEL_FORMAT_RGBX_8888) { bitmap->setIsOpaque(true); } if (w > 0 && h > 0) { bitmap->setPixelRef(pixels)->unref(); bitmap->lockPixels(); } else { // be safe with an empty bitmap. delete pixels; bitmap->setPixels(NULL); } return GraphicsJNI::createBitmap(env, bitmap, false, NULL);} |
由于對C++不熟,我這里就不敢多言了。其實到這里,算是對手機android源碼中通過組合鍵屏幕截圖的整個流程有個大體了解了,一般我們在改動中熟悉按鍵的捕獲原理,并且清楚調用的截屏函數(shù)接口即可,如果有興趣的,可以繼續(xù)探究更深的底層是如何實現(xiàn)的。