Android源码解析之截屏事件流程
今天这篇文章我们主要讲一下Android系统中的截屏事件处理流程。用过android系统手机的同学应该都知道,一般的android手机按下音量减少键和电源按键就会触发截屏事件(国内定制机做个修改的这里就不做考虑了)。那么这里的截屏事件是如何触发的呢?触发之后android系统是如何实现截屏操作的呢?带着这两个问题,开始我们的源码阅读流程。
我们知道这里的截屏事件是通过我们的按键操作触发的,所以这里就需要我们从android系统的按键触发模块开始看起,由于我们在不同的App页面,操作音量减少键和电源键都会触发系统的截屏处理,所以这里的按键触发逻辑应该是Android系统的全局按键处理逻辑。
在android系统中,由于我们的每一个Android界面都是一个Activity,而界面的显示都是通过Window对象实现的,每个Window对象实际上都是PhoneWindow的实例,而每个PhoneWindow对象都一个PhoneWindowManager对象,当我们在Activity界面执行按键操作的时候,在将按键的处理操作分发到App之前,首先会回调PhoneWindowManager中的dispatchUnhandledKey方法,该方法主要用于执行当前App处理按键之前的操作,我们具体看一下该方法的实现。
/**{@inheritDoc}*/ @Override publicKeyEventdispatchUnhandledKey(WindowStatewin,KeyEventevent,intpolicyFlags){ ... KeyEventfallbackEvent=null; if((event.getFlags()&KeyEvent.FLAG_FALLBACK)==0){ finalKeyCharacterMapkcm=event.getKeyCharacterMap(); finalintkeyCode=event.getKeyCode(); finalintmetaState=event.getMetaState(); finalbooleaninitialDown=event.getAction()==KeyEvent.ACTION_DOWN &&event.getRepeatCount()==0; //Checkforfallbackactionsspecifiedbythekeycharactermap. finalFallbackActionfallbackAction; if(initialDown){ fallbackAction=kcm.getFallbackAction(keyCode,metaState); }else{ fallbackAction=mFallbackActions.get(keyCode); } if(fallbackAction!=null){ ... finalintflags=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); }elseif(event.getAction()==KeyEvent.ACTION_UP){ mFallbackActions.remove(keyCode); fallbackAction.recycle(); } } } ... returnfallbackEvent; }
这里我们关注一下方法体中调用的:interceptFallback方法,通过调用该方法将处理按键的操作下发到该方法中,我们继续看一下该方法的实现逻辑。
privatebooleaninterceptFallback(WindowStatewin,KeyEventfallbackEvent,intpolicyFlags){ intactions=interceptKeyBeforeQueueing(fallbackEvent,policyFlags); if((actions&ACTION_PASS_TO_USER)!=0){ longdelayMillis=interceptKeyBeforeDispatching( win,fallbackEvent,policyFlags); if(delayMillis==0){ returntrue; } } returnfalse; }
然后我们看到在interceptFallback方法中我们调用了interceptKeyBeforeQueueing方法,通过阅读我们我们知道该方法主要实现了对截屏按键的处理流程,这样我们继续看一下interceptKeyBeforeWueueing方法的处理:
@Override publicintinterceptKeyBeforeQueueing(KeyEventevent,intpolicyFlags){ if(!mSystemBooted){ //Ifwehavenotyetbooted,don'tletkeyeventsdoanything. return0; } ... //Handlespecialkeys. switch(keyCode){ caseKeyEvent.KEYCODE_VOLUME_DOWN: caseKeyEvent.KEYCODE_VOLUME_UP: caseKeyEvent.KEYCODE_VOLUME_MUTE:{ if(mUseTvRouting){ //OnTVsvolumekeysnevergototheforegroundapp 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(); } } ... returnresult; }
可以发现这里首先判断当前系统是否已经boot完毕,若尚未启动完毕,则所有的按键操作都将失效,若启动完成,则执行后续的操作,这里我们只是关注音量减少按键和电源按键组合的处理事件。另外这里多说一句想安卓系统的HOME按键事件,MENU按键事件,进程列表按键事件等等都是在这里实现的,后续中我们会陆续介绍这方面的内容。
回到我们的interceptKeyBeforeQueueing方法,当我用按下音量减少按键的时候回进入到:caseKeyEvent.KEYCODE_VOLUME_MUTE分支并执行相应的逻辑,然后同时判断用户是否按下了电源键,若同时按下了电源键,则执行:
if(interactive&&!mScreenshotChordVolumeDownKeyTriggered &&(event.getFlags()&KeyEvent.FLAG_FALLBACK)==0){ mScreenshotChordVolumeDownKeyTriggered=true; mScreenshotChordVolumeDownKeyTime=event.getDownTime(); mScreenshotChordVolumeDownKeyConsumed=false; cancelPendingPowerKeyAction(); interceptScreenshotChord(); }
可以发现这里的interceptScreenshotChrod方法就是系统准备开始执行截屏操作的开始,我们继续看一下interceptcreenshotChord方法的实现。
privatevoidinterceptScreenshotChord(){ if(mScreenshotChordEnabled &&mScreenshotChordVolumeDownKeyTriggered&&mScreenshotChordPowerKeyTriggered &&!mScreenshotChordVolumeUpKeyTriggered){ finallongnow=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方法的具体实现。
privatelonggetScreenshotChordLongPressDelay(){ if(mKeyguardDelegate.isShowing()){ //Doublethetimeittakestotakeascreenshotfromthekeyguard return(long)(KEYGUARD_SCREENSHOT_CHORD_DELAY_MULTIPLIER* ViewConfiguration.get(mContext).getDeviceGlobalActionKeyTimeout()); } returnViewConfiguration.get(mContext).getDeviceGlobalActionKeyTimeout(); }
回到我们的interceptScreenshotChord方法,发送了异步消息之后系统最终会被我们发送的Runnable对象的run方法执行,这里关于异步消息的逻辑可参考:android源码解析之(二)–>异步消息机制
这样我们看一下Runnable类型的mScreenshotRunnable的run方法的实现:
privatefinalRunnablemScreenshotRunnable=newRunnable(){ @Override publicvoidrun(){ takeScreenshot(); } };
好吧,方法体中并未执行其他操作,直接就是调用了takeScreenshot方法,这样我们继续看一下takeScreenshot方法的实现。
privatevoidtakeScreenshot(){ synchronized(mScreenshotLock){ if(mScreenshotConnection!=null){ return; } ComponentNamecn=newComponentName("com.android.systemui", "com.android.systemui.screenshot.TakeScreenshotService"); Intentintent=newIntent(); intent.setComponent(cn); ServiceConnectionconn=newServiceConnection(){ @Override publicvoidonServiceConnected(ComponentNamename,IBinderservice){ synchronized(mScreenshotLock){ if(mScreenshotConnection!=this){ return; } Messengermessenger=newMessenger(service); Messagemsg=Message.obtain(null,1); finalServiceConnectionmyConn=this; Handlerh=newHandler(mHandler.getLooper()){ @Override publicvoidhandleMessage(Messagemsg){ synchronized(mScreenshotLock){ if(mScreenshotConnection==myConn){ mContext.unbindService(mScreenshotConnection); mScreenshotConnection=null; mHandler.removeCallbacks(mScreenshotTimeout); } } } }; msg.replyTo=newMessenger(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(RemoteExceptione){ } } } @Override publicvoidonServiceDisconnected(ComponentNamename){} }; if(mContext.bindServiceAsUser( intent,conn,Context.BIND_AUTO_CREATE,UserHandle.CURRENT)){ mScreenshotConnection=conn; mHandler.postDelayed(mScreenshotTimeout,10000); } } }
可以发现这里通过反射机制创建了一个TakeScreenshotService对象然后调用了bindServiceAsUser,这样就创建了TakeScreenshotService服务并在服务创建之后发送了一个异步消息。好了,我们看一下TakeScreenshotService的实现逻辑。
publicclassTakeScreenshotServiceextendsService{ privatestaticfinalStringTAG="TakeScreenshotService"; privatestaticGlobalScreenshotmScreenshot; privateHandlermHandler=newHandler(){ @Override publicvoidhandleMessage(Messagemsg){ switch(msg.what){ case1: finalMessengercallback=msg.replyTo; if(mScreenshot==null){ mScreenshot=newGlobalScreenshot(TakeScreenshotService.this); } mScreenshot.takeScreenshot(newRunnable(){ @Overridepublicvoidrun(){ Messagereply=Message.obtain(null,1); try{ callback.send(reply); }catch(RemoteExceptione){ } } },msg.arg1>0,msg.arg2>0); } } }; @Override publicIBinderonBind(Intentintent){ returnnewMessenger(mHandler).getBinder(); } }
可以发现在在TakeScreenshotService类的定义中有一个Handler成员变量,而我们在启动TakeScreentshowService的时候回发送一个异步消息,这样就会执行mHandler的handleMessage方法,然后在handleMessage方法中我们创建了一个GlobalScreenshow对象,然后执行了takeScreenshot方法,好吧,继续看一下takeScreentshot方法的执行逻辑。
/** *Takesascreenshotofthecurrentdisplayandshowsananimation. */ voidtakeScreenshot(Runnablefinisher,booleanstatusBarVisible,booleannavBarVisible){ //Weneedtoorientthescreenshotcorrectly(andtheSurfaceapiseemstotakescreenshots //onlyinthenaturalorientationofthedevice:!) mDisplay.getRealMetrics(mDisplayMetrics); float[]dims={mDisplayMetrics.widthPixels,mDisplayMetrics.heightPixels}; floatdegrees=getDegreesForRotation(mDisplay.getRotation()); booleanrequiresRotation=(degrees>0); if(requiresRotation){ //Getthedimensionsofthedeviceinitsnativeorientation mDisplayMatrix.reset(); mDisplayMatrix.preRotate(-degrees); mDisplayMatrix.mapPoints(dims); dims[0]=Math.abs(dims[0]); dims[1]=Math.abs(dims[1]); } //Takethescreenshot mScreenBitmap=SurfaceControl.screenshot((int)dims[0],(int)dims[1]); if(mScreenBitmap==null){ notifyScreenshotError(mContext,mNotificationManager); finisher.run(); return; } if(requiresRotation){ //Rotatethescreenshottothecurrentorientation Bitmapss=Bitmap.createBitmap(mDisplayMetrics.widthPixels, mDisplayMetrics.heightPixels,Bitmap.Config.ARGB_8888); Canvasc=newCanvas(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); //Recyclethepreviousbitmap mScreenBitmap.recycle(); mScreenBitmap=ss; } //Optimizations mScreenBitmap.setHasAlpha(false); mScreenBitmap.prepareToDraw(); //Startthepost-screenshotanimation 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方法,然后调用了:
//Takethescreenshot mScreenBitmap=SurfaceControl.screenshot((int)dims[0],(int)dims[1]);
方法,看注释,这里就是执行截屏事件的具体操作了,然后我看一下SurfaceControl.screenshot方法的具体实现,另外这里需要注意的是,截屏之后返回的是一个Bitmap对象,其实熟悉android绘制机制的童鞋应该知道android中所有显示能够显示的东西,在内存中表现都是Bitmap对象。
publicstaticBitmapscreenshot(intwidth,intheight){ //TODO:shouldtakethedisplayasaparameter IBinderdisplayToken=SurfaceControl.getBuiltInDisplay( SurfaceControl.BUILT_IN_DISPLAY_ID_MAIN); returnnativeScreenshot(displayToken,newRect(),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通知。
staticvoidnotifyScreenshotError(Contextcontext,NotificationManagernManager){ Resourcesr=context.getResources(); //Clearallexistingnotification,composethenewnotificationandshowit Notification.Builderb=newNotification.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)//oktoshowoutsidelockscreen .setCategory(Notification.CATEGORY_ERROR) .setAutoCancel(true) .setColor(context.getColor( com.android.internal.R.color.system_notification_accent_color)); Notificationn= newNotification.BigTextStyle(b) .bigText(r.getString(R.string.screenshot_failed_text)) .build(); nManager.notify(R.id.notification_screenshot,n); }
然后继续看takeScreenshot方法,判断截屏的图像是否需要旋转,若需要的话,则旋转图像:
if(requiresRotation){ //Rotatethescreenshottothecurrentorientation Bitmapss=Bitmap.createBitmap(mDisplayMetrics.widthPixels, mDisplayMetrics.heightPixels,Bitmap.Config.ARGB_8888); Canvasc=newCanvas(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); //Recyclethepreviousbitmap mScreenBitmap.recycle(); mScreenBitmap=ss; }
在takeScreenshot方法的最后若截屏成功,我们调用了:
//Startthepost-screenshotanimation startAnimation(finisher,mDisplayMetrics.widthPixels,mDisplayMetrics.heightPixels, statusBarVisible,navBarVisible);
开始截屏的动画,好吧,看一下动画效果的实现:
/** *Startstheanimationaftertakingthescreenshot */ privatevoidstartAnimation(finalRunnablefinisher,intw,inth,booleanstatusBarVisible, booleannavBarVisible){ //Addtheviewfortheanimation mScreenshotView.setImageBitmap(mScreenBitmap); mScreenshotLayout.requestFocus(); //Setuptheanimationwiththescreenshotjusttaken if(mScreenshotAnimation!=null){ mScreenshotAnimation.end(); mScreenshotAnimation.removeAllListeners(); } mWindowManager.addView(mScreenshotLayout,mWindowLayoutParams); ValueAnimatorscreenshotDropInAnim=createScreenshotDropInAnimation(); ValueAnimatorscreenshotFadeOutAnim=createScreenshotDropOutAnimation(w,h, statusBarVisible,navBarVisible); mScreenshotAnimation=newAnimatorSet(); mScreenshotAnimation.playSequentially(screenshotDropInAnim,screenshotFadeOutAnim); mScreenshotAnimation.addListener(newAnimatorListenerAdapter(){ @Override publicvoidonAnimationEnd(Animatoranimation){ //Savethescreenshotoncewehaveabitoftimenow saveScreenshotInWorkerThread(finisher); mWindowManager.removeView(mScreenshotLayout); //Clearanyreferencestothebitmap mScreenBitmap=null; mScreenshotView.setImageBitmap(null); } }); mScreenshotLayout.post(newRunnable(){ @Override publicvoidrun(){ //Playtheshuttersoundtonotifythatwe'vetakenascreenshot mCameraSound.play(MediaActionSound.SHUTTER_CLICK); mScreenshotView.setLayerType(View.LAYER_TYPE_HARDWARE,null); mScreenshotView.buildLayer(); mScreenshotAnimation.start(); } }); }
好吧,经过着一些列的操作之后我们实现了截屏之后的动画效果了,这里暂时不分析动画效果,我们看一下动画效果之后做了哪些?还记不记的一般情况下我们截屏之后都会收到一个截屏的notification通知?这里应该也是在其AnimatorListenerAdapter的onAnimationEnd方法中实现的,也就是动画执行完成之后,我们看一下其saveScreenshotInWorkerThread方法的实现:
/** *Createsanewworkerthreadandsavesthescreenshottothemediastore. */ privatevoidsaveScreenshotInWorkerThread(Runnablefinisher){ SaveImageInBackgroundDatadata=newSaveImageInBackgroundData(); 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=newSaveImageInBackgroundTask(mContext,data,mNotificationManager, R.id.notification_screenshot).execute(data); }
好吧,这里主要逻辑就是构造了一个SaveImageInBackgroundTask对象,看样子发送截屏成功的通知应该是在这里实现的,我们看一下SaveImageInBackgroundTask构造方法的实现逻辑:
SaveImageInBackgroundTask(Contextcontext,SaveImageInBackgroundDatadata, NotificationManagernManager,intnId){ ... //Showtheintermediatenotification mTickerAddSpace=!mTickerAddSpace; mNotificationId=nId; mNotificationManager=nManager; finallongnow=System.currentTimeMillis(); mNotificationBuilder=newNotification.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=newNotification.BigPictureStyle() .bigPicture(picture.createAshmemBitmap()); mNotificationBuilder.setStyle(mNotificationStyle); //For"public"situationswewanttoshowallthesameinfobut //omittheactualscreenshotimage. mPublicNotificationBuilder=newNotification.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()); Notificationn=mNotificationBuilder.build(); n.flags|=Notification.FLAG_NO_CLEAR; mNotificationManager.notify(nId,n); //Onthetablet,thelargeiconmakesthenotificationappearasifitisclickable(and //onsmalldevices,thelargeiconisnotshown)sodefershowingthelargeiconuntil //wecomposethefinalpost-savenotificationbelow. mNotificationBuilder.setLargeIcon(icon.createAshmemBitmap()); //Butwestilldon'tsetitfortheexpandedview,allowingthesmallIcontoshowhere. mNotificationStyle.bigLargeIcon((Bitmap)null); }
可以发现在构造方法的后面狗仔了一个NotificationBuilder对象,然后发送了一个截屏成功的Notification,
这样我们在截屏动画之后就收到了Notification的通知了。
总结:
在PhoneWindowManager的dispatchUnhandledKey方法中处理App无法处理的按键事件,当然也包括音量减少键和电源按键的组合按键
通过一系列的调用启动TakeScreenshotService服务,并通过其执行截屏的操作。
具体的截屏代码是在native层实现的。
截屏操作时候,若截屏失败则直接发送截屏失败的notification通知。
截屏之后,若截屏成功,则先执行截屏的动画,并在动画效果执行完毕之后,发送截屏成功的notification的通知
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持毛票票。