詳解有關(guān)Android截圖與錄屏功能的學(xué)習(xí)
簡單的截屏和錄屏功能。
因為MediaProjection是5.0以上才出現(xiàn)的,所以今天所講述功能實現(xiàn),只在5.0以上的系統(tǒng)有效。
截屏:
步驟如下:
1:獲取MediaProjectionManager
2:通過MediaProjectionManager.createScreenCaptureIntent()獲取Intent
3:通過startActivityForResult傳入Intent然后在onActivityResult中通過MediaProjectionManager.getMediaProjection(resultCode,data)獲取MediaProjection
4:創(chuàng)建ImageReader,構(gòu)建VirtualDisplay
5:最后就是通過ImageReader截圖,就可以從ImageReader里獲得Image對象。
6:將Image對象轉(zhuǎn)換成bitmap
實現(xiàn):
步驟已經(jīng)給出了,我們就按照步驟來實現(xiàn)代碼吧。
首先MediaProjectionManager是系統(tǒng)服務(wù),我們通過getSystemService(MEDIA_PROJECTION_SERVICE)獲取它
projectionManager = (MediaProjectionManager) getSystemService(MEDIA_PROJECTION_SERVICE);
然后調(diào)用startActivityForResult傳入projectionManager.createScreenCaptureIntent()創(chuàng)建的Intent
startActivityForResult(projectionManager.createScreenCaptureIntent(),SCREEN_SHOT);
緊接著我們就可以在onActivityResult(int requestCode, int resultCode, Intent data)中通過resultCode和data來獲取MediaProjection
@Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { if(requestCode == SCREEN_SHOT){ if(resultCode == RESULT_OK){ //獲取MediaProjection mediaProjection = projectionManager.getMediaProjection(requestCode,data); } } }
然后就是創(chuàng)建ImageReader和VirtualDisplay
imageReader = ImageReader.newInstance(width, height, PixelFormat.RGBA_8888, 1); if(imageReader!=null){ Log.d(TAG, "imageReader Successful"); } mediaProjection.createVirtualDisplay("ScreenShout", width,height,dpi, DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR, imageReader.getSurface(),null,null);
這里我們依次講解一下。
首先是ImageReader.newInstance方法:
public static ImageReader newInstance(int width, int height, int format, int maxImages)
方法里接收四個參數(shù)。
前兩個width,height是用來指定生成圖像的寬和高。
第三個參數(shù)format是圖像的格式,這個格式必須是ImageFormat或PixelFormat中的一個,這兩個Format里有很多格式,大家可以點進去看看,我們例子中使用的是PixelFormat.RGBA_8888格式(需要注意的是并不是所有的格式都被ImageReader支持,比如說ImageFormat.NV21)。
第四個參數(shù)是maxImages,這個參數(shù)指的是你想同時在ImageReader里獲取到的Image對象的個數(shù),這個參數(shù)我不是很懂,我不理解同時的意思。我的理解是ImageReader是一個類似數(shù)組的東西,然后我們可以通過acquireLatestImage()或acquireNextImage()方法來得到里面的Image對象(可能有誤,僅供參考)。這個值應(yīng)該設(shè)置的越小越好,但是得大于0,所以我們上面設(shè)置的是1。
然后我們看看mediaProjection.createVirtualDisplay方法:
createVirtualDisplay(@NonNull String name, int width, int height, int dpi, int flags, @Nullable Surface surface, @Nullable VirtualDisplay.Callback callback, @Nullable Handler handler)
首先這個方法返回的是VirtualDisplay。
前四個不用說了,分別是VirtualDisplay的名字,寬,高和dpi。
第五個參數(shù),大家可以點 DisplayManager查看所有的flags,我沒有具體的研究過,在本次要實現(xiàn)的例子里,除了VIRTUAL_DISPLAY_FLAG_SECURE這個會報錯,其他的flags效果都一樣。
第六個參數(shù),是一個Surface。我這里表達一下我的理解,當(dāng)VirtualDisplay被創(chuàng)建出來時,也就是createVirtualDisplay調(diào)用后,你在真實屏幕上的每一幀都會輸入到Surface參數(shù)里。也就是說,如果你放個SurfaceView,然后傳入SurfaceView的Surface那么你在屏幕上的操作都會顯示在SurfaceView里(這里我們后面錄屏?xí)v)。我們這里傳入的是ImageReader的Surface。這其中的邏輯我的理解是這樣的,真實屏幕的每一幀都都會傳給ImageReader,根據(jù)ImageReader的maxImages參數(shù),比如說maxImages是2,那么ImageReader始終保持兩幀圖片,但這兩幀圖片是一直隨著真實屏幕的操作而更新的(不知道大家有沒有聽懂)。
第七個參數(shù),是一個回調(diào)函數(shù),在VirtualDisplay狀態(tài)改變時調(diào)用。因為我們這里沒有,所以傳null。
第八個參數(shù),這里我給出原文:“The Handler on which the callback should be invoked, or null if the callback should be invoked on the calling thread's main Looper.”因為我翻譯不好。不過和普通的Handler使用場景類似。
現(xiàn)在我們ImageReader和VirtualDisplay,接下來我們就可以通過ImageReader的acquireLatestImage()或acquireNextImage()來得到Image對象了。
SystemClock.sleep(1000); Image image = imageReader.acquireNextImage();
這里有個坑,就是你在獲取Image的時候,得先暫停1秒左右,不然就會獲取失敗(原因未知)。
現(xiàn)在我們有了Image對象,但是Image對象并不能直接作為UI資源被使用,我們可以將它轉(zhuǎn)換成Bitmap對象。
int width = image.getWidth(); int height = image.getHeight(); final Image.Plane[] planes = image.getPlanes(); final ByteBuffer buffer = planes[0].getBuffer(); int pixelStride = planes[0].getPixelStride(); int rowStride = planes[0].getRowStride(); int rowPadding = rowStride - pixelStride * width; bitmap = Bitmap.createBitmap(width + rowPadding / pixelStride, height, Bitmap.Config.ARGB_8888); bitmap.copyPixelsFromBuffer(buffer); image.close();
這里最主要的邏輯就是像素與字節(jié)的轉(zhuǎn)換,我們需要將Image對象的字節(jié)流寫進Bitmap里,但是Bitmap接收的是像素格式的。
我們一行一行來看:
首先獲取image對象的寬和高,注意width和height是像素格式的。
然后獲取ByteBuffer,里面存放的就是圖片的字節(jié)流,是字節(jié)格式的。我是這么理解的,ByteBuffer里面是一長串的字節(jié)序列,按照某種格式分成行列就變成了圖片。
然后獲取PixelStride,這指的是兩個像素的距離(就是一個像素頭部到相鄰像素的頭部),這是字節(jié)格式的。
RowStride是一行占用的距離(就是一行像素頭部到相鄰行像素的頭部),這個大小和width有關(guān),這里需要注意,因為內(nèi)存對齊的原因,所以每行會有一些空余。這個值也是字節(jié)格式的。
緊接著我們需要創(chuàng)建一個Bitmap用來接受Image的buffer的輸入,buffer是字節(jié)流,它會按照我們設(shè)置的format轉(zhuǎn)換成像素,所以這里最重要的一個地方就是Bitmap創(chuàng)建的大小,因為高度就是行數(shù)所以就是height,但是寬度因為上面說的內(nèi)存對齊問題會有些空余,所以我們要先求出空余部分,然后加上width。
int rowPadding = rowStride - pixelStride * width;
這句話用整行的距離減去了一行里像素及空隙占用的距離,剩下的就是空余部分。但是這個是字節(jié)格式的。我們將它除以pixelStride,也就是一個像素及空隙占用的字節(jié)大小,就轉(zhuǎn)換成了像素格式。
然后:
width + rowPadding / pixelStride
這個就是一行里像素的占用了,我們將它傳給Bitmap:
bitmap = Bitmap.createBitmap(width + rowPadding / pixelStride, height, Bitmap.Config.ARGB_8888);
創(chuàng)建出合適大小的Bitmap,然后把Image的buffer傳給它,就成功的將Image對象轉(zhuǎn)換成了Bitmap。
這里我可能講的不清楚,我給大家畫了張圖:
上面的一小格一小格是一塊塊像素。
好了,現(xiàn)在我們已經(jīng)獲取到了bitmap了,我們可以把它放到ImageView里顯示一下,我寫了一個例子,效果如下:
點擊按鈕,彈出一個對話框請求截屏,點擊立即開始的話,截屏就會顯示在下面的ImageView里。
截屏就這樣,我已經(jīng)盡力了,╮(╯▽╰)╭
錄屏:
步驟:
錄屏的前三步和截屏是一樣的,出現(xiàn)分歧點的地方在于VirtualDisplay創(chuàng)建時傳入的Surface,還記得我們上面說的嗎,說在創(chuàng)建VirtualDisplay的時候,傳入一個SurfaceView的Surface的話,那么你在真實屏幕上的操作,都會重現(xiàn)在SurfaceView上。我們來試一下:
mediaProjection.createVirtualDisplay("ScreenShout", width,height,dpi, DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR, surfaceView.getHolder().getSurface(),null,null);
我們在Surface參數(shù)中傳入一個SurfaceView的Surface
效果如下:
可以看到我們放了一個Button,放了一個ImageView,放了一個SurfaceView。
點擊Button,然后點立即開始之后,真實屏幕就映射到了SurfaceView里。
所以當(dāng)創(chuàng)建VirtualDisplay時,真實屏幕就映射到了Surface,也就是我們可以再Surface里拿到屏幕的一個輸入。那我們要錄屏的話,就只要把Surface轉(zhuǎn)換成我們需要的格式就行了,在本篇文章的例子中,我們會將Surface對象轉(zhuǎn)換成mp4格式。這就需要用到MediaCodec類和MediaMuxer類。MediaCodec生成一個Surface用來接收屏幕的輸出并按照格式編碼,然后傳給MediaMuxer用來封裝成mp4格式的視頻。
//第一個參數(shù)是mime類型,我們傳入video/avc //第二第三個參數(shù)是寬和高 MediaFormat format = MediaFormat.createVideoFormat("video/avc", width, height); //COLOR_FormatSurface這里表明數(shù)據(jù)將是一個graphicbuffer元數(shù)據(jù) format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface); //設(shè)置碼率,碼率越大視頻越清晰,相對的占用內(nèi)存也要更大 format.setInteger(MediaFormat.KEY_BIT_RATE, 6000000); //設(shè)置幀數(shù) format.setInteger(MediaFormat.KEY_FRAME_RATE, 30); //設(shè)置兩個關(guān)鍵幀的間隔,這個值你設(shè)置成多少對我們這個例子都沒啥影響 //這個值做視頻的朋友可能會懂,反正我不是很懂,大概就是你預(yù)覽的時候,比如你設(shè)置為10,那么你10秒內(nèi)的預(yù)覽圖都是同一張 format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 10); //創(chuàng)建一個MediaCodec實例 mediaCodec = MediaCodec.createEncoderByType("video/avc"); //第一個參數(shù)將我們上面設(shè)置的format傳進去 //第二個參數(shù)是Surface,如果我們需要讀取MediaCodec編碼后的數(shù)據(jù)就要傳,但我們這里不需要所以傳null //第三個參數(shù)關(guān)于加解密的,我們不需要,傳null //第四個參數(shù)是一個確定的標(biāo)志位,也就是我們現(xiàn)在傳的這個 mediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); //獲取MediaCodec的surface,這個surface其實就是一個入口,屏幕作為輸入源就會進入這個入口,然后交給MediaCodec編碼 surface = mediaCodec.createInputSurface(); mediaCodec.start();
上面講了MediaCodec的創(chuàng)建,我們也可以從中看到屏幕數(shù)據(jù)是怎么進入MediaCodec的。具體的我已經(jīng)注釋了。
接下來我們創(chuàng)建一個MediaMuxer對象:
//第一個參數(shù)是輸出的地址 //第二個參數(shù)是輸出的格式,我們設(shè)置的是mp4格式 mediaMuxer = new MediaMuxer(filePath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
然后創(chuàng)建VirtualDisplay,把MediaCodec的surface傳進去:
virtualDisplay = mediaProjection.createVirtualDisplay(TAG + "-display", width, height, dpi, DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC, surface, null, null);
最后就是視頻的編碼與轉(zhuǎn)換MP4還有保存了:
private void recordVirtualDisplay() { while (!mQuit.get()) { //dequeueOutputBuffer方法你可以這么理解,它會出列一個輸出buffer(你可以理解為一幀畫面),返回值是這一幀畫面的順序位置(類似于數(shù)組的下標(biāo)) //第二個參數(shù)是超時時間,如果超過這個時間了還沒成功出列,那么就會跳過這一幀,去出列下一幀,并返回INFO_TRY_AGAIN_LATER標(biāo)志位 int index = mediaCodec.dequeueOutputBuffer(bufferInfo, 10000); //當(dāng)格式改變的時候嗎,我們需要重新設(shè)置格式 //在本例中,只第一次開始的時候會返回這個值 if (index == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { resetOutputFormat(); } else if (index >= 0) {//這里說明dequeueOutputBuffer執(zhí)行正常 //這里執(zhí)行我們轉(zhuǎn)換成mp4的邏輯 encodeToVideoTrack(index); mediaCodec.releaseOutputBuffer(index, false); } } } //這里是將數(shù)據(jù)傳給MediaMuxer,將其轉(zhuǎn)換成mp4 private void encodeToVideoTrack(int index) { //通過index獲取到ByteBuffer(可以理解為一幀) ByteBuffer encodedData = mediaCodec.getOutputBuffer(index); //當(dāng)bufferInfo返回這個標(biāo)志位時,就說明已經(jīng)傳完數(shù)據(jù)了,我們將bufferInfo.size設(shè)為0,準(zhǔn)備將其回收 if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) { bufferInfo.size = 0; } if (bufferInfo.size == 0) { encodedData = null; } if (encodedData != null) { encodedData.position(bufferInfo.offset);//設(shè)置我們該從哪個位置讀取數(shù)據(jù) encodedData.limit(bufferInfo.offset + bufferInfo.size);//設(shè)置我們該讀多少數(shù)據(jù) //這里將數(shù)據(jù)寫入 //第一個參數(shù)是每一幀畫面要放置的順序 //第二個是要寫入的數(shù)據(jù) //第三個參數(shù)是bufferInfo,這個數(shù)據(jù)包含的是encodedData的offset和size mediaMuxer.writeSampleData(videoTrackIndex, encodedData, bufferInfo); } } //這個方法其實就是設(shè)置MediaMuxer的Format private void resetOutputFormat() { //將MediaCodec的Format設(shè)置給MediaMuxer MediaFormat newFormat = mediaCodec.getOutputFormat(); //獲取videoTrackIndex,這個值是每一幀畫面要放置的順序 videoTrackIndex = mediaMuxer.addTrack(newFormat); mediaMuxer.start(); muxerStarted = true; }
好了,錄屏到此結(jié)束了。
我們來看下實例演示:
總結(jié):
這篇博客寫的真是費時費力,果然水平未到就不該強行寫文。
我不知道我是不是寫清楚了,但還是希望大家看了之后能有一絲絲的收獲,這就是對我最大的鼓勵。
本篇博客的錄屏代碼參考自:ScreenRecorder
本篇博客的實例代碼:
github項目地址:https://github.com/ChenTianSaber/ScreenRecorderShoter
源碼下載地址:ScreenRecorderShoter_jb51.rar
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
Android CoordinatorLayout高級用法之自定義Behavior
這篇文章主要介紹了Android CoordinatorLayout高級用法之自定義Behavior,具有一定的參考價值,感興趣的小伙伴們可以參考一下2018-02-02Android Jetpack架構(gòu)組件Lifecycle詳解
這篇文章主要介紹了Android Jetpack架構(gòu)組件Lifecycle詳解,Lifecycle是Jetpack架構(gòu)組件中用來感知生命周期的組件,使用Lifecycles可以幫助我們寫出和生命周期相關(guān)更簡潔更易維護的代碼。對此感興趣的小伙伴可以來學(xué)習(xí)一下2020-07-07Android GestureDetector手勢滑動使用實例講解
這篇文章主要為大家詳細(xì)介紹了Android GestureDetector手勢滑動使用實例,文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2016-07-07Android RecyclerView實現(xiàn)下拉列表功能
這篇文章主要介紹了Android RecyclerView實現(xiàn)下拉列表功能,下拉展開更多選項,具有一定的實用性,感興趣的小伙伴們可以參考一下2016-11-11Android學(xué)習(xí)系列一用按鈕實現(xiàn)顯示時間
這篇文章主要介紹了Android學(xué)習(xí)系列一用按鈕實現(xiàn)顯示時間的相關(guān)資料,需要的朋友可以參考下2016-05-05