溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

怎么在Android應(yīng)用中實(shí)現(xiàn)一個(gè)截圖與錄屏功能

發(fā)布時(shí)間:2020-12-02 16:48:47 來源:億速云 閱讀:461 作者:Leah 欄目:移動(dòng)開發(fā)

這篇文章給大家介紹怎么在Android應(yīng)用中實(shí)現(xiàn)一個(gè)截圖與錄屏功能,內(nèi)容非常詳細(xì),感興趣的小伙伴們可以參考借鑒,希望對(duì)大家能有所幫助。

截屏:

步驟如下:

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對(duì)象。

6:將Image對(duì)象轉(zhuǎn)換成bitmap

實(shí)現(xiàn):

步驟已經(jīng)給出了,我們就按照步驟來實(shí)現(xiàn)代碼吧。

首先MediaProjectionManager是系統(tǒng)服務(wù),我們通過getSystemService(MEDIA_PROJECTION_SERVICE)獲取它

復(fù)制代碼 代碼如下:

projectionManager = (MediaProjectionManager) getSystemService(MEDIA_PROJECTION_SERVICE);

然后調(diào)用startActivityForResult傳入projectionManager.createScreenCaptureIntent()創(chuàng)建的Intent

復(fù)制代碼 代碼如下:

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方法:

復(fù)制代碼 代碼如下:

public static ImageReader newInstance(int width, int height, int format, int maxImages)

方法里接收四個(gè)參數(shù)。
前兩個(gè)width,height是用來指定生成圖像的寬和高。

第三個(gè)參數(shù)format是圖像的格式,這個(gè)格式必須是ImageFormat或PixelFormat中的一個(gè),這兩個(gè)Format里有很多格式,大家可以點(diǎn)進(jìn)去看看,我們例子中使用的是PixelFormat.RGBA_8888格式(需要注意的是并不是所有的格式都被ImageReader支持,比如說ImageFormat.NV21)。

第四個(gè)參數(shù)是maxImages,這個(gè)參數(shù)指的是你想同時(shí)在ImageReader里獲取到的Image對(duì)象的個(gè)數(shù),這個(gè)參數(shù)我不是很懂,我不理解同時(shí)的意思。我的理解是ImageReader是一個(gè)類似數(shù)組的東西,然后我們可以通過acquireLatestImage()或acquireNextImage()方法來得到里面的Image對(duì)象(可能有誤,僅供參考)。這個(gè)值應(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)

首先這個(gè)方法返回的是VirtualDisplay。

前四個(gè)不用說了,分別是VirtualDisplay的名字,寬,高和dpi。

第五個(gè)參數(shù),大家可以點(diǎn) DisplayManager查看所有的flags,我沒有具體的研究過,在本次要實(shí)現(xiàn)的例子里,除了VIRTUAL_DISPLAY_FLAG_SECURE這個(gè)會(huì)報(bào)錯(cuò),其他的flags效果都一樣。

第六個(gè)參數(shù),是一個(gè)Surface。我這里表達(dá)一下我的理解,當(dāng)VirtualDisplay被創(chuàng)建出來時(shí),也就是createVirtualDisplay調(diào)用后,你在真實(shí)屏幕上的每一幀都會(huì)輸入到Surface參數(shù)里。也就是說,如果你放個(gè)SurfaceView,然后傳入SurfaceView的Surface那么你在屏幕上的操作都會(huì)顯示在SurfaceView里(這里我們后面錄屏?xí)v)。我們這里傳入的是ImageReader的Surface。這其中的邏輯我的理解是這樣的,真實(shí)屏幕的每一幀都都會(huì)傳給ImageReader,根據(jù)ImageReader的maxImages參數(shù),比如說maxImages是2,那么ImageReader始終保持兩幀圖片,但這兩幀圖片是一直隨著真實(shí)屏幕的操作而更新的(不知道大家有沒有聽懂)。

第七個(gè)參數(shù),是一個(gè)回調(diào)函數(shù),在VirtualDisplay狀態(tài)改變時(shí)調(diào)用。因?yàn)槲覀冞@里沒有,所以傳null。

第八個(gè)參數(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.”因?yàn)槲曳g不好。不過和普通的Handler使用場(chǎng)景類似。

現(xiàn)在我們ImageReader和VirtualDisplay,接下來我們就可以通過ImageReader的acquireLatestImage()或acquireNextImage()來得到Image對(duì)象了。

SystemClock.sleep(1000);
Image image = imageReader.acquireNextImage();

這里有個(gè)坑,就是你在獲取Image的時(shí)候,得先暫停1秒左右,不然就會(huì)獲取失敗(原因未知)。

現(xiàn)在我們有了Image對(duì)象,但是Image對(duì)象并不能直接作為UI資源被使用,我們可以將它轉(zhuǎn)換成Bitmap對(duì)象。

    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對(duì)象的字節(jié)流寫進(jìn)Bitmap里,但是Bitmap接收的是像素格式的。
我們一行一行來看:

首先獲取image對(duì)象的寬和高,注意width和height是像素格式的。

然后獲取ByteBuffer,里面存放的就是圖片的字節(jié)流,是字節(jié)格式的。我是這么理解的,ByteBuffer里面是一長(zhǎng)串的字節(jié)序列,按照某種格式分成行列就變成了圖片。

然后獲取PixelStride,這指的是兩個(gè)像素的距離(就是一個(gè)像素頭部到相鄰像素的頭部),這是字節(jié)格式的。

RowStride是一行占用的距離(就是一行像素頭部到相鄰行像素的頭部),這個(gè)大小和width有關(guān),這里需要注意,因?yàn)閮?nèi)存對(duì)齊的原因,所以每行會(huì)有一些空余。這個(gè)值也是字節(jié)格式的。

緊接著我們需要?jiǎng)?chuàng)建一個(gè)Bitmap用來接受Image的buffer的輸入,buffer是字節(jié)流,它會(huì)按照我們?cè)O(shè)置的format轉(zhuǎn)換成像素,所以這里最重要的一個(gè)地方就是Bitmap創(chuàng)建的大小,因?yàn)楦叨染褪切袛?shù)所以就是height,但是寬度因?yàn)樯厦嬲f的內(nèi)存對(duì)齊問題會(huì)有些空余,所以我們要先求出空余部分,然后加上width。

int rowPadding = rowStride - pixelStride * width;

這句話用整行的距離減去了一行里像素及空隙占用的距離,剩下的就是空余部分。但是這個(gè)是字節(jié)格式的。我們將它除以pixelStride,也就是一個(gè)像素及空隙占用的字節(jié)大小,就轉(zhuǎn)換成了像素格式。
然后:

width + rowPadding / pixelStride

這個(gè)就是一行里像素的占用了,我們將它傳給Bitmap:

復(fù)制代碼 代碼如下:

bitmap = Bitmap.createBitmap(width + rowPadding / pixelStride, height, Bitmap.Config.ARGB_8888);

創(chuàng)建出合適大小的Bitmap,然后把Image的buffer傳給它,就成功的將Image對(duì)象轉(zhuǎn)換成了Bitmap。

這里我可能講的不清楚,我給大家畫了張圖:

怎么在Android應(yīng)用中實(shí)現(xiàn)一個(gè)截圖與錄屏功能

上面的一小格一小格是一塊塊像素。

好了,現(xiàn)在我們已經(jīng)獲取到了bitmap了,我們可以把它放到ImageView里顯示一下,我寫了一個(gè)例子,效果如下:

怎么在Android應(yīng)用中實(shí)現(xiàn)一個(gè)截圖與錄屏功能

點(diǎn)擊按鈕,彈出一個(gè)對(duì)話框請(qǐng)求截屏,點(diǎn)擊立即開始的話,截屏就會(huì)顯示在下面的ImageView里。

截屏就這樣,我已經(jīng)盡力了,╮(╯▽╰)╭

錄屏:

步驟:

錄屏的前三步和截屏是一樣的,出現(xiàn)分歧點(diǎn)的地方在于VirtualDisplay創(chuàng)建時(shí)傳入的Surface,還記得我們上面說的嗎,說在創(chuàng)建VirtualDisplay的時(shí)候,傳入一個(gè)SurfaceView的Surface的話,那么你在真實(shí)屏幕上的操作,都會(huì)重現(xiàn)在SurfaceView上。我們來試一下:

mediaProjection.createVirtualDisplay("ScreenShout",
        width,height,dpi,
        DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
        surfaceView.getHolder().getSurface(),null,null);

我們?cè)赟urface參數(shù)中傳入一個(gè)SurfaceView的Surface

效果如下:

怎么在Android應(yīng)用中實(shí)現(xiàn)一個(gè)截圖與錄屏功能

可以看到我們放了一個(gè)Button,放了一個(gè)ImageView,放了一個(gè)SurfaceView。

點(diǎn)擊Button,然后點(diǎn)立即開始之后,真實(shí)屏幕就映射到了SurfaceView里。

所以當(dāng)創(chuàng)建VirtualDisplay時(shí),真實(shí)屏幕就映射到了Surface,也就是我們可以再Surface里拿到屏幕的一個(gè)輸入。那我們要錄屏的話,就只要把Surface轉(zhuǎn)換成我們需要的格式就行了,在本篇文章的例子中,我們會(huì)將Surface對(duì)象轉(zhuǎn)換成mp4格式。這就需要用到MediaCodec類和MediaMuxer類。MediaCodec生成一個(gè)Surface用來接收屏幕的輸出并按照格式編碼,然后傳給MediaMuxer用來封裝成mp4格式的視頻。

    //第一個(gè)參數(shù)是mime類型,我們傳入video/avc
    //第二第三個(gè)參數(shù)是寬和高
    MediaFormat format = MediaFormat.createVideoFormat("video/avc", width, height);
    //COLOR_FormatSurface這里表明數(shù)據(jù)將是一個(gè)graphicbuffer元數(shù)據(jù)
    format.setInteger(MediaFormat.KEY_COLOR_FORMAT,
        MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
    //設(shè)置碼率,碼率越大視頻越清晰,相對(duì)的占用內(nèi)存也要更大
    format.setInteger(MediaFormat.KEY_BIT_RATE, 6000000);
    //設(shè)置幀數(shù)
    format.setInteger(MediaFormat.KEY_FRAME_RATE, 30);
    //設(shè)置兩個(gè)關(guān)鍵幀的間隔,這個(gè)值你設(shè)置成多少對(duì)我們這個(gè)例子都沒啥影響
    //這個(gè)值做視頻的朋友可能會(huì)懂,反正我不是很懂,大概就是你預(yù)覽的時(shí)候,比如你設(shè)置為10,那么你10秒內(nèi)的預(yù)覽圖都是同一張
    format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 10);
    //創(chuàng)建一個(gè)MediaCodec實(shí)例
    mediaCodec = MediaCodec.createEncoderByType("video/avc");
    //第一個(gè)參數(shù)將我們上面設(shè)置的format傳進(jìn)去
    //第二個(gè)參數(shù)是Surface,如果我們需要讀取MediaCodec編碼后的數(shù)據(jù)就要傳,但我們這里不需要所以傳null
    //第三個(gè)參數(shù)關(guān)于加解密的,我們不需要,傳null
    //第四個(gè)參數(shù)是一個(gè)確定的標(biāo)志位,也就是我們現(xiàn)在傳的這個(gè)
    mediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
    //獲取MediaCodec的surface,這個(gè)surface其實(shí)就是一個(gè)入口,屏幕作為輸入源就會(huì)進(jìn)入這個(gè)入口,然后交給MediaCodec編碼
    surface = mediaCodec.createInputSurface();
    mediaCodec.start();

上面講了MediaCodec的創(chuàng)建,我們也可以從中看到屏幕數(shù)據(jù)是怎么進(jìn)入MediaCodec的。具體的我已經(jīng)注釋了。

接下來我們創(chuàng)建一個(gè)MediaMuxer對(duì)象:

//第一個(gè)參數(shù)是輸出的地址
//第二個(gè)參數(shù)是輸出的格式,我們?cè)O(shè)置的是mp4格式
mediaMuxer = new MediaMuxer(filePath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);

然后創(chuàng)建VirtualDisplay,把MediaCodec的surface傳進(jìn)去:

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方法你可以這么理解,它會(huì)出列一個(gè)輸出buffer(你可以理解為一幀畫面),返回值是這一幀畫面的順序位置(類似于數(shù)組的下標(biāo))
      //第二個(gè)參數(shù)是超時(shí)時(shí)間,如果超過這個(gè)時(shí)間了還沒成功出列,那么就會(huì)跳過這一幀,去出列下一幀,并返回INFO_TRY_AGAIN_LATER標(biāo)志位
      int index = mediaCodec.dequeueOutputBuffer(bufferInfo, 10000);
      //當(dāng)格式改變的時(shí)候嗎,我們需要重新設(shè)置格式
      //在本例中,只第一次開始的時(shí)候會(huì)返回這個(gè)值
      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返回這個(gè)標(biāo)志位時(shí),就說明已經(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è)置我們?cè)搹哪膫€(gè)位置讀取數(shù)據(jù)
      encodedData.limit(bufferInfo.offset + bufferInfo.size);//設(shè)置我們?cè)撟x多少數(shù)據(jù)
      //這里將數(shù)據(jù)寫入
      //第一個(gè)參數(shù)是每一幀畫面要放置的順序
      //第二個(gè)是要寫入的數(shù)據(jù)
      //第三個(gè)參數(shù)是bufferInfo,這個(gè)數(shù)據(jù)包含的是encodedData的offset和size
      mediaMuxer.writeSampleData(videoTrackIndex, encodedData, bufferInfo);

    }
  }

  //這個(gè)方法其實(shí)就是設(shè)置MediaMuxer的Format
  private void resetOutputFormat() {
    //將MediaCodec的Format設(shè)置給MediaMuxer
    MediaFormat newFormat = mediaCodec.getOutputFormat();
    //獲取videoTrackIndex,這個(gè)值是每一幀畫面要放置的順序
    videoTrackIndex = mediaMuxer.addTrack(newFormat);
    mediaMuxer.start();
    muxerStarted = true;
  }

關(guān)于怎么在Android應(yīng)用中實(shí)現(xiàn)一個(gè)截圖與錄屏功能就分享到這里了,希望以上內(nèi)容可以對(duì)大家有一定的幫助,可以學(xué)到更多知識(shí)。如果覺得文章不錯(cuò),可以把它分享出去讓更多的人看到。

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI