Android 截图功能源码的分析
android 截图功能源码的分析
一般没有修改rom的android原生系统截图功能的组合键是音量减+开机键;今天我们从源码角度来分析截图功能是如何在源码中实现的。
在android系统中,由于我们的每一个android界面都是一个activity,而界面的显示都是通过window对象实现的,每个window对象实际上都是phonewindow的实例,而每个phonewindow对象都对应一个phonewindowmanager对象,当我们在activity界面执行按键操作的时候,在将按键的处理操作分发到app之前,首先会回调phonewindowmanager中的dispatchunhandledkey方法,该方法主要用于执行当前app处理按键之前的操作,我们具体看一下该方法的实现。
/** {@inheritdoc} */ @override public keyevent dispatchunhandledkey(windowstate win, keyevent event, int policyflags) { ... keyevent fallbackevent = null; if ((event.getflags() & keyevent.flag_fallback) == 0) { final keycharactermap kcm = event.getkeycharactermap(); final int keycode = event.getkeycode(); final int metastate = event.getmetastate(); final boolean initialdown = event.getaction() == keyevent.action_down && event.getrepeatcount() == 0; // check for fallback actions specified by the key character map. final fallbackaction fallbackaction; if (initialdown) { fallbackaction = kcm.getfallbackaction(keycode, metastate); } else { fallbackaction = mfallbackactions.get(keycode); } if (fallbackaction != null) { ... final int flags = event.getflags() | keyevent.flag_fallback; fallbackevent = keyevent.obtain( event.getdowntime(), event.geteventtime(), event.getaction(), fallbackaction.keycode, event.getrepeatcount(), fallbackaction.metastate, event.getdeviceid(), event.getscancode(), flags, event.getsource(), null); if (!interceptfallback(win, fallbackevent, policyflags)) { fallbackevent.recycle(); fallbackevent = null; } if (initialdown) { mfallbackactions.put(keycode, fallbackaction); } else if (event.getaction() == keyevent.action_up) { mfallbackactions.remove(keycode); fallbackaction.recycle(); } } } ... return fallbackevent; }
这里我们关注一下方法体中调用的:interceptfallback方法,通过调用该方法将处理按键的操作下发到该方法中,我们继续看一下该方法的实现逻辑。
private boolean interceptfallback(windowstate win, keyevent fallbackevent, int policyflags) { int actions = interceptkeybeforequeueing(fallbackevent, policyflags); if ((actions & action_pass_to_user) != 0) { long delaymillis = interceptkeybeforedispatching( win, fallbackevent, policyflags); if (delaymillis == 0) { return true; } } return false; }
然后我们看到在interceptfallback方法中我们调用了interceptkeybeforequeueing方法,通过阅读我们我们知道该方法主要实现了对截屏按键的处理流程,这样我们继续看一下interceptkeybeforewueueing方法的处理:
@override public int interceptkeybeforequeueing(keyevent event, int policyflags) { 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 (musetvrouting) { // on tvs volume keys never go to the foreground app result &= ~action_pass_to_user; } if (keycode == keyevent.keycode_volume_down) { if (down) { if (interactive && !mscreenshotchordvolumedownkeytriggered && (event.getflags() & keyevent.flag_fallback) == 0) { mscreenshotchordvolumedownkeytriggered = true; mscreenshotchordvolumedownkeytime = event.getdowntime(); mscreenshotchordvolumedownkeyconsumed = false; cancelpendingpowerkeyaction(); interceptscreenshotchord(); } } else { mscreenshotchordvolumedownkeytriggered = false; cancelpendingscreenshotchordaction(); } } ... return result; }
可以发现这里首先判断当前系统是否已经boot完毕,若尚未启动完毕,则所有的按键操作都将失效,若启动完成,则执行后续的操作,这里我们只是关注音量减少按键和电源按键组合的处理事件。另外这里多说一句想安卓系统的home按键事件,menu按键事件,进程列表按键事件等等都是在这里实现的,后续中我们会陆续介绍这方面的内容。
回到我们的interceptkeybeforequeueing方法,当我用按下音量减少按键的时候回进入到:case keyevent.keycode_volume_mute分支并执行相应的逻辑,然后同时判断用户是否按下了电源键,若同时按下了电源键,则执行:
if (interactive && !mscreenshotchordvolumedownkeytriggered && (event.getflags() & keyevent.flag_fallback) == 0) { mscreenshotchordvolumedownkeytriggered = true; mscreenshotchordvolumedownkeytime = event.getdowntime(); mscreenshotchordvolumedownkeyconsumed = false; cancelpendingpowerkeyaction(); interceptscreenshotchord(); }
可以发现这里的interceptscreenshotchrod方法就是系统准备开始执行截屏操作的开始,我们继续看一下interceptcreenshotchord方法的实现。
private void interceptscreenshotchord() { 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()); } } }
在方法体中我们最终会执行发送一个延迟的异步消息,请求执行截屏的操作而这里的延时时间,若当前输入框是打开状态,则延时时间为输入框关闭时间加上系统配置的按键超时时间,若当前输入框没有打开则直接是系统配置的按键超时处理时间,可看一下getscreenshotchordlongpressdelay方法的具体实现。
private long getscreenshotchordlongpressdelay() { if (mkeyguarddelegate.isshowing()) { // double the time it takes to take a screenshot from the keyguard return (long) (keyguard_screenshot_chord_delay_multiplier * viewconfiguration.get(mcontext).getdeviceglobalactionkeytimeout()); } return viewconfiguration.get(mcontext).getdeviceglobalactionkeytimeout(); }
回到我们的interceptscreenshotchord方法,发送了异步消息之后系统最终会被我们发送的runnable对象的run方法执行;这样我们看一下runnable类型的mscreenshotrunnable的run方法的实现:
private final runnable mscreenshotrunnable = new runnable() { @override public void run() { takescreenshot(); } };
好吧,方法体中并未执行其他操作,直接就是调用了takescreenshot方法,这样我们继续看一下takescreenshot方法的实现。
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.bindserviceasuser( intent, conn, context.bind_auto_create, userhandle.current)) { mscreenshotconnection = conn; mhandler.postdelayed(mscreenshottimeout, 10000); } } }
可以发现这里通过反射机制创建了一个takescreenshotservice对象然后调用了bindserviceasuser,这样就创建了takescreenshotservice服务并在服务创建之后发送了一个异步消息。好了,我们看一下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); } } }; @override public ibinder onbind(intent intent) { return new messenger(mhandler).getbinder(); } }
可以发现在在takescreenshotservice类的定义中有一个handler成员变量,而我们在启动takescreentshowservice的时候回发送一个异步消息,这样就会执行mhandler的handlemessage方法,然后在handlemessage方法中我们创建了一个globalscreenshow对象,然后执行了takescreenshot方法,好吧,继续看一下takescreentshot方法的执行逻辑。
/** * 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; } 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); }
可以看到这里后两个参数:statusbarvisible,navbarvisible是否可见,而这两个参数在我们
phonewindowmanager.takescreenshot方法传递的: if (mstatusbar != null && mstatusbar.isvisiblelw()) msg.arg1 = 1; if (mnavigationbar != null && mnavigationbar.isvisiblelw()) msg.arg2 = 1;
可见若果mstatusbar可见,则传递的statusbarvisible为true,若mnavigationbar可见,则传递的navbarvisible为true。然后我们在截屏的时候判断nstatusbar是否可见,mnavigationbar是否可见,若可见的时候则截屏同样将其截屏出来。继续回到我们的takescreenshot方法,然后调用了:
// take the screenshot mscreenbitmap = surfacecontrol.screenshot((int) dims[0], (int) dims[1]);
方法,看注释,这里就是执行截屏事件的具体操作了,然后我看一下surfacecontrol.screenshot方法的具体实现,另外这里需要注意的是,截屏之后返回的是一个bitmap对象,其实熟悉android绘制机制的童鞋应该知道android中所有显示能够显示的东西,在内存中表现都是bitmap对象。
public static bitmap screenshot(int width, int height) { // todo: should take the display as a parameter ibinder displaytoken = surfacecontrol.getbuiltindisplay( surfacecontrol.built_in_display_id_main); return nativescreenshot(displaytoken, new rect(), width, height, 0, 0, true, false, surface.rotation_0); }
好吧,这里调用的是nativescreenshot方法,它是一个native方法,具体的实现在jni层,这里就不做过多的介绍了。继续回到我们的takescreenshot方法,在调用了截屏方法screentshot之后,判断是否截屏成功:
if (mscreenbitmap == null) { notifyscreenshoterror(mcontext, mnotificationmanager); finisher.run(); return; }
若截屏之后,截屏的bitmap对象为空,这里判断截屏失败,调用了notifyscreenshoterror方法,发送截屏失败的notification通知。
static void notifyscreenshoterror(context context, notificationmanager nmanager) { resources r = context.getresources(); // clear all existing notification, compose the new notification and show it notification.builder b = new notification.builder(context) .setticker(r.getstring(r.string.screenshot_failed_title)) .setcontenttitle(r.getstring(r.string.screenshot_failed_title)) .setcontenttext(r.getstring(r.string.screenshot_failed_text)) .setsmallicon(r.drawable.stat_notify_image_error) .setwhen(system.currenttimemillis()) .setvisibility(notification.visibility_public) // ok to show outside lockscreen .setcategory(notification.category_error) .setautocancel(true) .setcolor(context.getcolor( com.android.internal.r.color.system_notification_accent_color)); notification n = new notification.bigtextstyle(b) .bigtext(r.getstring(r.string.screenshot_failed_text)) .build(); nmanager.notify(r.id.notification_screenshot, n); }
然后继续看takescreenshot方法,判断截屏的图像是否需要旋转,若需要的话,则旋转图像:
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; }
在takescreenshot方法的最后若截屏成功,我们调用了:
// start the post-screenshot animation startanimation(finisher, mdisplaymetrics.widthpixels, mdisplaymetrics.heightpixels, statusbarvisible, navbarvisible);
开始截屏的动画,好吧,看一下动画效果的实现:
/** * starts the animation after taking the screenshot */ private void startanimation(final runnable finisher, int w, int h, boolean statusbarvisible, boolean navbarvisible) { // add the view for the animation mscreenshotview.setimagebitmap(mscreenbitmap); mscreenshotlayout.requestfocus(); // setup the animation with the screenshot just taken if (mscreenshotanimation != null) { mscreenshotanimation.end(); mscreenshotanimation.removealllisteners(); } mwindowmanager.addview(mscreenshotlayout, mwindowlayoutparams); valueanimator screenshotdropinanim = createscreenshotdropinanimation(); valueanimator screenshotfadeoutanim = createscreenshotdropoutanimation(w, h, statusbarvisible, navbarvisible); mscreenshotanimation = new animatorset(); mscreenshotanimation.playsequentially(screenshotdropinanim, screenshotfadeoutanim); mscreenshotanimation.addlistener(new animatorlisteneradapter() { @override public void onanimationend(animator animation) { // save the screenshot once we have a bit of time now savescreenshotinworkerthread(finisher); mwindowmanager.removeview(mscreenshotlayout); // clear any references to the bitmap mscreenbitmap = null; mscreenshotview.setimagebitmap(null); } }); mscreenshotlayout.post(new runnable() { @override public void run() { // play the shutter sound to notify that we've taken a screenshot mcamerasound.play(mediaactionsound.shutter_click); mscreenshotview.setlayertype(view.layer_type_hardware, null); mscreenshotview.buildlayer(); mscreenshotanimation.start(); } }); }
好吧,经过着一些列的操作之后我们实现了截屏之后的动画效果了,这里暂时不分析动画效果,我们看一下动画效果之后做了哪些?还记不记的一般情况下我们截屏之后都会收到一个截屏的notification通知?这里应该也是在其animatorlisteneradapter的onanimationend方法中实现的,也就是动画执行完成之后,我们看一下其savescreenshotinworkerthread方法的实现:
/** * creates a new worker thread and saves the screenshot to the media store. */ private void savescreenshotinworkerthread(runnable finisher) { saveimageinbackgrounddata data = new saveimageinbackgrounddata(); data.context = mcontext; data.image = mscreenbitmap; data.iconsize = mnotificationiconsize; data.finisher = finisher; data.previewwidth = mpreviewwidth; data.previewheight = mpreviewheight; if (msaveinbgtask != null) { msaveinbgtask.cancel(false); } msaveinbgtask = new saveimageinbackgroundtask(mcontext, data, mnotificationmanager, r.id.notification_screenshot).execute(data); }
好吧,这里主要逻辑就是构造了一个saveimageinbackgroundtask对象,看样子发送截屏成功的通知应该是在这里实现的,我们看一下saveimageinbackgroundtask构造方法的实现逻辑:
saveimageinbackgroundtask(context context, saveimageinbackgrounddata data, notificationmanager nmanager, int nid) { ... // show the intermediate notification mtickeraddspace = !mtickeraddspace; mnotificationid = nid; mnotificationmanager = nmanager; final long now = system.currenttimemillis(); mnotificationbuilder = new notification.builder(context) .setticker(r.getstring(r.string.screenshot_saving_ticker) + (mtickeraddspace ? " " : "")) .setcontenttitle(r.getstring(r.string.screenshot_saving_title)) .setcontenttext(r.getstring(r.string.screenshot_saving_text)) .setsmallicon(r.drawable.stat_notify_image) .setwhen(now) .setcolor(r.getcolor(com.android.internal.r.color.system_notification_accent_color)); mnotificationstyle = new notification.bigpicturestyle() .bigpicture(picture.createashmembitmap()); mnotificationbuilder.setstyle(mnotificationstyle); // for "public" situations we want to show all the same info but // omit the actual screenshot image. mpublicnotificationbuilder = new notification.builder(context) .setcontenttitle(r.getstring(r.string.screenshot_saving_title)) .setcontenttext(r.getstring(r.string.screenshot_saving_text)) .setsmallicon(r.drawable.stat_notify_image) .setcategory(notification.category_progress) .setwhen(now) .setcolor(r.getcolor( com.android.internal.r.color.system_notification_accent_color)); mnotificationbuilder.setpublicversion(mpublicnotificationbuilder.build()); notification n = mnotificationbuilder.build(); n.flags |= notification.flag_no_clear; mnotificationmanager.notify(nid, n); // on the tablet, the large icon makes the notification appear as if it is clickable (and // on small devices, the large icon is not shown) so defer showing the large icon until // we compose the final post-save notification below. mnotificationbuilder.setlargeicon(icon.createashmembitmap()); // but we still don't set it for the expanded view, allowing the smallicon to show here. mnotificationstyle.biglargeicon((bitmap) null); }
可以发现在构造方法的后面狗仔了一个notificationbuilder对象,然后发送了一个截屏成功的notification,这样我们在截屏动画之后就收到了notification的通知了。
总结:
一般默认情况下按下音量减少键和开机键会执行截图动作,程序执行的入口就在在phonewindowmanager的dispatchunhandledkey方法中;然后通过takescreenshotservice服务执行截图逻辑;通过nativie方法获取截图的bitmap,如果失败调用失败通知栏消息,如果成功调用截图动画后发送成功通知栏消息。
如有疑问请留言或者到本站社区交流讨论,感谢阅读,希望能帮助到大家,谢谢大家对本站的支持!