Android中使用程式碼截圖的各種方法總結

yangxi_001發表於2014-07-02

1,基於Android SDK的截圖方法

(1)主要就是利用SDK提供的View.getDrawingCache()方法。網上已經有很多的例項了。首先建立一個android project,然後進行Layout,畫一個按鍵(res/layout/main.xml):

<?xmlversion="1.0"encoding="utf-8"?>
<LinearLayoutxmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    >
<TextView
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:text="@string/hello"
    />
<Button
  android:text="NiceButton"
  android:id="@+id/my_button"
  android:layout_width="fill_parent"
  android:layout_height="wrap_content"
  android:layout_alignParentBottom="true"></Button>
</LinearLayout>

HelloAndroid.java實現程式碼為:

packagecom.example.helloandroid;
 
importjava.io.FileOutputStream;
importjava.text.SimpleDateFormat;
importjava.util.Date;
importjava.util.Locale;
 
importandroid.app.Activity;
importandroid.graphics.Bitmap;
importandroid.os.Bundle;
importandroid.view.View;
importandroid.view.View.OnClickListener;
importandroid.widget.Button;
 
publicclassHelloAndroidextendsActivity {
 
  privateButton button;
 
  /** Called when the activity is first created. */
  @Override
  publicvoidonCreate(Bundle savedInstanceState) {
 
    super.onCreate(savedInstanceState);
    this.setContentView(R.layout.main);
    this.button = (Button) this.findViewById(R.id.my_button);
    this.button.setOnClickListener(newOnClickListener() {
 
      publicvoidonClick(View v) {
        SimpleDateFormat sdf = newSimpleDateFormat(
            "yyyy-MM-dd_HH-mm-ss", Locale.US);
        String fname = "/sdcard/"+ sdf.format(newDate()) + ".png";
        View view = v.getRootView();
        view.setDrawingCacheEnabled(true);
        view.buildDrawingCache();
        Bitmap bitmap = view.getDrawingCache();
        if(bitmap != null) {
          System.out.println("bitmap got!");
          try{
            FileOutputStream out = newFileOutputStream(fname);
            bitmap.compress(Bitmap.CompressFormat.PNG,100, out);
            System.out.println("file " + fname + "output done.");
          }catch(Exception e) {
            e.printStackTrace();
          }
        }else{
          System.out.println("bitmap is NULL!");
        }
      }
 
    });
 
  }
}

這個程式碼會在按下app中按鍵的時候自動在手機的/sdcard/目錄下生成一個時間戳命名的png截圖檔案。

這種截圖有一個問題,就是隻能截到一部分,比如電池指示部分就截不出來了。

(2)在APK中呼叫“adb shell screencap -pfilepath” 命令

該命令讀取系統的framebuffer,需要獲得系統許可權:
(1). 在AndroidManifest.xml檔案中新增
<uses-permissionandroid:name="android.permission.READ_FRAME_BUFFER"/>
(2). 修改APK為系統許可權,將APK放到原始碼中編譯, 修改Android.mk
 LOCAL_CERTIFICATE := platform

  1. publicvoid takeScreenShot(){

  2.    String mSavedPath = Environment.getExternalStorageDirectory()+File. separator + "screenshot.png" ;

  3. try {                    

  4.           Runtime. getRuntime().exec("screencap -p " + mSavedPath);

  5.    } catch (Exception e) {

  6.           e.printStackTrace();

  7.    }


(3).利用系統的API,實現Screenshot,這部分程式碼是系統隱藏的,需要在原始碼下編譯,

    1).修改Android.mk, 新增系統許可權
          LOCAL_CERTIFICATE := platform
         2).修改AndroidManifest.xml 檔案,新增
許可權
<uses-permissionandroid:name="android.permission.READ_FRAME_BUFFER"/>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
      public boolean takeScreenShot(String imagePath){
                     
                    
                     
             if(imagePath.equals("" )){
                      imagePath = Environment.getExternalStorageDirectory()+File. separator+"Screenshot.png" ;
             }
                     
          Bitmap mScreenBitmap;
          WindowManager mWindowManager;
          DisplayMetrics mDisplayMetrics;
          Display mDisplay;
                  
          mWindowManager = (WindowManager) mcontext.getSystemService(Context.WINDOW_SERVICE);
          mDisplay = mWindowManager.getDefaultDisplay();
          mDisplayMetrics = new DisplayMetrics();
          mDisplay.getRealMetrics(mDisplayMetrics);
                                 
          float[] dims = {mDisplayMetrics.widthPixels , mDisplayMetrics.heightPixels };
          mScreenBitmap = Surface. screenshot((int) dims[0], ( int) dims[1]);
                     
          if (mScreenBitmap == null) {  
                 return false ;
          }
                  
       try {
          FileOutputStream out = new FileOutputStream(imagePath);
          mScreenBitmap.compress(Bitmap.CompressFormat. PNG, 100, out);
             
        catch (Exception e) {
                
                
          return false ;
        }       
                            
       return true ;
}

2 基於Android ddmlib進行截圖

[java] view plaincopy
  1. public class ScreenShot {  
  2.  private BufferedImage image = null;  
  3.  /** 
  4.   * @param args 
  5.   */  
  6.  public static void main(String[] args) {  
  7.   // TODO Auto-generated method stub  
  8.   AndroidDebugBridge.init(false); //  
  9.   ScreenShot screenshot = new ScreenShot();  
  10.   IDevice device = screenshot.getDevice();  
  11.     
  12.   for (int i = 0; i < 10; i++) {  
  13.    Date date=new Date();  
  14.    SimpleDateFormat df=new SimpleDateFormat("MM-dd-HH-mm-ss");   
  15.    String nowTime = df.format(date);  
  16.    screenshot.getScreenShot(device, "Robotium" + nowTime);  
  17.    try {  
  18.     Thread.sleep(1000);  
  19.    } catch (InterruptedException e) {  
  20.     // TODO Auto-generated catch block  
  21.     e.printStackTrace();  
  22.    }  
  23.   }  
  24.  }  
  25.   
  26.    
  27.  public void getScreenShot(IDevice device,String filename) {  
  28.   RawImage rawScreen = null;  
  29.   try {  
  30.    rawScreen = device.getScreenshot();  
  31.   } catch (TimeoutException e) {  
  32.    // TODO Auto-generated catch block  
  33.    e.printStackTrace();  
  34.   } catch (AdbCommandRejectedException e) {  
  35.    // TODO Auto-generated catch block  
  36.    e.printStackTrace();  
  37.   } catch (IOException e) {  
  38.    // TODO Auto-generated catch block  
  39.    e.printStackTrace();  
  40.   }  
  41.   if (rawScreen != null) {  
  42.    Boolean landscape = false;  
  43.    int width2 = landscape ? rawScreen.height : rawScreen.width;  
  44.    int height2 = landscape ? rawScreen.width : rawScreen.height;  
  45.    if (image == null) {  
  46.     image = new BufferedImage(width2, height2,  
  47.       BufferedImage.TYPE_INT_RGB);  
  48.    } else {  
  49.     if (image.getHeight() != height2 || image.getWidth() != width2) {  
  50.      image = new BufferedImage(width2, height2,  
  51.        BufferedImage.TYPE_INT_RGB);  
  52.     }  
  53.    }  
  54.    int index = 0;  
  55.    int indexInc = rawScreen.bpp >> 3;  
  56.    for (int y = 0; y < rawScreen.height; y++) {  
  57.     for (int x = 0; x < rawScreen.width; x++, index += indexInc) {  
  58.      int value = rawScreen.getARGB(index);  
  59.      if (landscape)  
  60.       image.setRGB(y, rawScreen.width - x - 1, value);  
  61.      else  
  62.       image.setRGB(x, y, value);  
  63.     }  
  64.    }  
  65.    try {  
  66.     ImageIO.write((RenderedImage) image, "PNG"new File("D:/"  
  67.       + filename + ".jpg"));  
  68.    } catch (IOException e) {  
  69.     // TODO Auto-generated catch block  
  70.     e.printStackTrace();  
  71.    }  
  72.   }  
  73.  }  
  74.   
  75.  /** 
  76.   * 獲取得到device物件 
  77.   * @return 
  78.   */  
  79.  private IDevice getDevice(){  
  80.   IDevice device;  
  81.   AndroidDebugBridge bridge = AndroidDebugBridge  
  82.     .createBridge("adb"true);//如果程式碼有問題請檢視API,修改此處的引數值試一下  
  83.   waitDevicesList(bridge);  
  84.   IDevice devices[] = bridge.getDevices();  
  85.   device = devices[0];  
  86.   return device;  
  87.  }  
  88.    
  89.  /** 
  90.   * 等待查詢device 
  91.   * @param bridge 
  92.   */  
  93.  private void waitDevicesList(AndroidDebugBridge bridge) {  
  94.   int count = 0;  
  95.   while (bridge.hasInitialDeviceList() == false) {  
  96.    try {  
  97.     Thread.sleep(500);   
  98.     count++;  
  99.    } catch (InterruptedException e) {  
  100.    }  
  101.    if (count > 240) {  
  102.     System.err.print("等待獲取裝置超時");  
  103.     break;  
  104.    }  
  105.   }  
  106.  }  

3 Android本地程式設計(Native Programming)讀取framebuffer


(1)命令列,框架的截圖功能是通過framebuffer來實現的,所以我們先來介紹一下framebuffer。

framebuffer介紹
幀緩衝(framebuffer)是Linux為顯示裝置提供的一個介面,把視訊記憶體抽象後的一種裝置,他允許上層應用程式在圖形模式下直接對顯示緩衝區進行 讀寫操作。這種操作是抽象的,統一的。使用者不必關心物理視訊記憶體的位置、換頁機制等等具體細節。這些都是由Framebuffer裝置驅動來完成的。
Linux FrameBuffer 本質上只是提供了對圖形裝置的硬體抽象,在開發者看來,FrameBuffer 是一塊顯示快取,往顯示快取中寫入特定格式的資料就意味著向螢幕輸出內容。所以說FrameBuffer就是一塊白板。例如對於初始化為16 位色的FrameBuffer 來說, FrameBuffer中的兩個位元組代表螢幕上一個點,從上到下,從左至右,螢幕位置與記憶體地址是順序的線性關係。
幀快取有個地址,是在記憶體裡。我們通過不停的向frame buffer中寫入資料, 顯示控制器就自動的從frame buffer中取資料並顯示出來。全部的圖形都共享記憶體中同一個幀快取。

Android截圖實現思路
Android系統是基於Linux核心的,所以也存在framebuffer這個裝置,我們要實現截圖的話只要能獲取到framebuffer中的資料,然後把資料轉換成圖片就可以了,android中的framebuffer資料是存放在 /dev/graphics/fb0 檔案中的,所以我們只需要來獲取這個檔案的資料就可以得到當前螢幕的內容。
現在我們的測試程式碼執行時候是通過RC(remote controller)方式來執行被測應用的,那就需要在PC機上來訪問模擬器或者真機上的framebuffer資料,這個的話可以通過android的ADB命令來實現。

具體實現


/***********************************************************************
  *
  *   ScreenShot.java
  ***********************************************************************/
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.DataInput;
import java.io.EOFException;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import javax.imageio.ImageIO;
import org.openqa.selenium.OutputType;
import org.openqa.selenium.internal.Base64Encoder;
import com.google.common.io.Closeables;
import com.google.common.io.LittleEndianDataInputStream;

/**
 */
public class ScreenShot {

    /**
     * @param args
     * @throws InterruptedException 
     */
    public static void main(String[] args) throws InterruptedException {    
        try {
            //解析度大小,後續可以通過程式碼來獲取到當前的解析度
            int xResolution = 320;
            int yResolution = 480;
            //執行adb命令,把framebuffer中內容儲存到fb1檔案中
             Runtime.getRuntime().exec("adb pull /dev/graphics/fb0 C:/fb1");
             //等待幾秒保證framebuffer中的資料都被儲存下來,如果沒有儲存完成進行讀取操作會有IO異常
             Thread.sleep(15000);
             //讀取檔案中的資料
             InputStream in = (InputStream)new FileInputStream("C:/fb1");
             DataInput frameBuffer = new LittleEndianDataInputStream(in);
             
             BufferedImage screenImage = new BufferedImage(
                     xResolution, yResolution, BufferedImage.TYPE_INT_ARGB);
                 int[] oneLine = new int[xResolution];
                for (int y = 0; y < yResolution; y++) {
                    //從frameBuffer中計算出rgb值
                    convertToRgba32(frameBuffer, oneLine);
                    //把rgb值設定到image物件中
                    screenImage.setRGB(0, y, xResolution, 1, oneLine, 0, xResolution);
                }
                Closeables.closeQuietly(in);
                
                ByteArrayOutputStream rawPngStream = new ByteArrayOutputStream();
                try {
                      if (!ImageIO.write(screenImage, "png", rawPngStream)) {
                        throw new RuntimeException(
                            "This Java environment does not support converting to PNG.");
                      }
                    } catch (IOException exception) {
                      // This should never happen because rawPngStream is an in-memory stream.
                     System.out.println("IOException=" + exception);
                    }
                byte[] rawPngBytes = rawPngStream.toByteArray();
                String base64Png = new Base64Encoder().encode(rawPngBytes);
                
                File screenshot = OutputType.FILE.convertFromBase64Png(base64Png);
                System.out.println("screenshot==" + screenshot.toString());
                screenshot.renameTo(new File("C:\\screenshottemp.png"));
                
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
            System.out.println(e);
        }
    }
    
    
    public static void convertToRgba32(DataInput frameBuffer, int[] into) {
        try {
            for (int x = 0; x < into.length; x++) {
                try{
                int rgb = frameBuffer.readShort() & 0xffff;
                int red = rgb >> 11;
                red = (red << 3) | (red >> 2);
                int green = (rgb >> 5) & 63;
                green = (green << 2) | (green >> 4);
                int blue = rgb & 31;
                blue = (blue << 3) | (blue >> 2);
                into[x] = 0xff000000 | (red << 16) | (green << 8) | blue;
                }catch (EOFException e){
                    System.out.println("EOFException=" + e);
                }
              }
        } catch (IOException exception) {
            System.out.println("convertToRgba32Exception=" + exception);
      }
    }
    
}

(2)

[java] view plaincopy
  1. 首先是直接移植SystemUI的程式碼,實現截圖效果,這部分的程式碼就不貼出來了,直接去下載程式碼吧, 關鍵的程式碼沒有幾句,最最主要的是:Surface.screenshot(),請看程式碼吧。  
  2.   
  3. [java]  
  4. <SPAN style="FONT-SIZE: 16px">package org.winplus.ss;   
  5.    
  6. import java.io.File;   
  7. import java.io.FileNotFoundException;   
  8. import java.io.FileOutputStream;   
  9. import java.io.IOException;   
  10. import java.text.SimpleDateFormat;   
  11. import java.util.Date;   
  12.    
  13. import android.app.Activity;   
  14. import android.content.Context;   
  15. import android.graphics.Bitmap;   
  16. import android.graphics.Canvas;   
  17. import android.graphics.Matrix;   
  18. import android.os.Bundle;   
  19. import android.util.DisplayMetrics;   
  20. import android.util.Log;   
  21. import android.view.Display;   
  22. import android.view.Surface;   
  23. import android.view.WindowManager;   
  24. import android.os.SystemProperties;   
  25.    
  26. public class SimpleScreenshotActivity extends Activity {   
  27.    
  28.     private Display mDisplay;   
  29.     private WindowManager mWindowManager;   
  30.     private DisplayMetrics mDisplayMetrics;   
  31.     private Bitmap mScreenBitmap;   
  32.     private Matrix mDisplayMatrix;   
  33.    
  34.     @Override   
  35.     public void onCreate(Bundle savedInstanceState) {   
  36.         super.onCreate(savedInstanceState);   
  37.         setContentView(R.layout.main);   
  38.    
  39.         new Thread(new Runnable() {   
  40.    
  41.             @Override   
  42.             public void run() {   
  43.                 takeScreenshot();   
  44.    
  45.             }   
  46.         }).start();   
  47.     }   
  48.    
  49.     private float getDegreesForRotation(int value) {   
  50.         switch (value) {   
  51.         case Surface.ROTATION_90:   
  52.             return 360f - 90f;   
  53.         case Surface.ROTATION_180:   
  54.             return 360f - 180f;   
  55.         case Surface.ROTATION_270:   
  56.             return 360f - 270f;   
  57.         }   
  58.         return 0f;   
  59.     }   
  60.    
  61.     private void takeScreenshot() {   
  62.         mWindowManager = (WindowManager) getSystemService(Context.WINDOW_SERVICE);   
  63.         mDisplay = mWindowManager.getDefaultDisplay();   
  64.         mDisplayMetrics = new DisplayMetrics();   
  65.         mDisplay.getRealMetrics(mDisplayMetrics);   
  66.         mDisplayMatrix = new Matrix();   
  67.         float[] dims = { mDisplayMetrics.widthPixels,   
  68.                 mDisplayMetrics.heightPixels };   
  69.    
  70.         int value = mDisplay.getRotation();   
  71.         String hwRotation = SystemProperties.get("ro.sf.hwrotation""0");   
  72.         if (hwRotation.equals("270") || hwRotation.equals("90")) {   
  73.             value = (value + 3) % 4;   
  74.         }   
  75.         float degrees = getDegreesForRotation(value);   
  76.    
  77.         boolean requiresRotation = (degrees > 0);   
  78.         if (requiresRotation) {   
  79.             // Get the dimensions of the device in its native orientation    
  80.             mDisplayMatrix.reset();   
  81.             mDisplayMatrix.preRotate(-degrees);   
  82.             mDisplayMatrix.mapPoints(dims);   
  83.    
  84.             dims[0] = Math.abs(dims[0]);   
  85.             dims[1] = Math.abs(dims[1]);   
  86.         }   
  87.    
  88.         mScreenBitmap = Surface.screenshot((int) dims[0], (int) dims[1]);   
  89.    
  90.         if (requiresRotation) {   
  91.             // Rotate the screenshot to the current orientation    
  92.             Bitmap ss = Bitmap.createBitmap(mDisplayMetrics.widthPixels,   
  93.                     mDisplayMetrics.heightPixels, Bitmap.Config.ARGB_8888);   
  94.             Canvas c = new Canvas(ss);   
  95.             c.translate(ss.getWidth() / 2, ss.getHeight() / 2);   
  96.             c.rotate(degrees);   
  97.             c.translate(-dims[0] / 2, -dims[1] / 2);   
  98.             c.drawBitmap(mScreenBitmap, 00null);   
  99.             c.setBitmap(null);   
  100.             mScreenBitmap = ss;   
  101.         }   
  102.    
  103.         // If we couldn't take the screenshot, notify the user    
  104.         if (mScreenBitmap == null) {   
  105.             return;   
  106.         }   
  107.    
  108.         // Optimizations    
  109.         mScreenBitmap.setHasAlpha(false);   
  110.         mScreenBitmap.prepareToDraw();   
  111.            
  112.         try {   
  113.             saveBitmap(mScreenBitmap);   
  114.         } catch (IOException e) {   
  115.             System.out.println(e.getMessage());   
  116.         }   
  117.     }   
  118.    
  119.     public void saveBitmap(Bitmap bitmap) throws IOException {   
  120.         String imageDate = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss")   
  121.                 .format(new Date(System.currentTimeMillis()));   
  122.         File file = new File("/mnt/sdcard/Pictures/"+imageDate+".png");   
  123.         if(!file.exists()){   
  124.             file.createNewFile();   
  125.         }   
  126.         FileOutputStream out;   
  127.         try {   
  128.             out = new FileOutputStream(file);   
  129.             if (bitmap.compress(Bitmap.CompressFormat.PNG, 70, out)) {   
  130.                 out.flush();   
  131.                 out.close();   
  132.             }   
  133.         } catch (FileNotFoundException e) {   
  134.             e.printStackTrace();   
  135.         } catch (IOException e) {   
  136.             e.printStackTrace();   
  137.         }   
  138.     }   
  139. }   
  140. </SPAN>   
  141.   
  142. package org.winplus.ss;  
  143.   
  144. import java.io.File;  
  145. import java.io.FileNotFoundException;  
  146. import java.io.FileOutputStream;  
  147. import java.io.IOException;  
  148. import java.text.SimpleDateFormat;  
  149. import java.util.Date;  
  150.   
  151. import android.app.Activity;  
  152. import android.content.Context;  
  153. import android.graphics.Bitmap;  
  154. import android.graphics.Canvas;  
  155. import android.graphics.Matrix;  
  156. import android.os.Bundle;  
  157. import android.util.DisplayMetrics;  
  158. import android.util.Log;  
  159. import android.view.Display;  
  160. import android.view.Surface;  
  161. import android.view.WindowManager;  
  162. import android.os.SystemProperties;  
  163.   
  164. public class SimpleScreenshotActivity extends Activity {  
  165.   
  166.  private Display mDisplay;  
  167.  private WindowManager mWindowManager;  
  168.  private DisplayMetrics mDisplayMetrics;  
  169.  private Bitmap mScreenBitmap;  
  170.  private Matrix mDisplayMatrix;  
  171.   
  172.  @Override  
  173.  public void onCreate(Bundle savedInstanceState) {  
  174.   super.onCreate(savedInstanceState);  
  175.   setContentView(R.layout.main);  
  176.   
  177.   new Thread(new Runnable() {  
  178.   
  179.    @Override  
  180.    public void run() {  
  181.     takeScreenshot();  
  182.   
  183.    }  
  184.   }).start();  
  185.  }  
  186.   
  187.  private float getDegreesForRotation(int value) {  
  188.   switch (value) {  
  189.   case Surface.ROTATION_90:  
  190.    return 360f - 90f;  
  191.   case Surface.ROTATION_180:  
  192.    return 360f - 180f;  
  193.   case Surface.ROTATION_270:  
  194.    return 360f - 270f;  
  195.   }  
  196.   return 0f;  
  197.  }  
  198.   
  199.  private void takeScreenshot() {  
  200.   mWindowManager = (WindowManager) getSystemService(Context.WINDOW_SERVICE);  
  201.   mDisplay = mWindowManager.getDefaultDisplay();  
  202.   mDisplayMetrics = new DisplayMetrics();  
  203.   mDisplay.getRealMetrics(mDisplayMetrics);  
  204.   mDisplayMatrix = new Matrix();  
  205.   float[] dims = { mDisplayMetrics.widthPixels,  
  206.     mDisplayMetrics.heightPixels };  
  207.   
  208.   int value = mDisplay.getRotation();  
  209.   String hwRotation = SystemProperties.get("ro.sf.hwrotation""0");  
  210.   if (hwRotation.equals("270") || hwRotation.equals("90")) {  
  211.    value = (value + 3) % 4;  
  212.   }  
  213.   float degrees = getDegreesForRotation(value);  
  214.   
  215.   boolean requiresRotation = (degrees > 0);  
  216.   if (requiresRotation) {  
  217.    // Get the dimensions of the device in its native orientation  
  218.    mDisplayMatrix.reset();  
  219.    mDisplayMatrix.preRotate(-degrees);  
  220.    mDisplayMatrix.mapPoints(dims);  
  221.   
  222.    dims[0] = Math.abs(dims[0]);  
  223.    dims[1] = Math.abs(dims[1]);  
  224.   }  
  225.   
  226.   mScreenBitmap = Surface.screenshot((int) dims[0], (int) dims[1]);  
  227.   
  228.   if (requiresRotation) {  
  229.             // Rotate the screenshot to the current orientation  
  230.             Bitmap ss = Bitmap.createBitmap(mDisplayMetrics.widthPixels,  
  231.                     mDisplayMetrics.heightPixels, Bitmap.Config.ARGB_8888);  
  232.             Canvas c = new Canvas(ss);  
  233.             c.translate(ss.getWidth() / 2, ss.getHeight() / 2);  
  234.             c.rotate(degrees);  
  235.             c.translate(-dims[0] / 2, -dims[1] / 2);  
  236.             c.drawBitmap(mScreenBitmap, 00null);  
  237.             c.setBitmap(null);  
  238.             mScreenBitmap = ss;  
  239.         }  
  240.   
  241.         // If we couldn't take the screenshot, notify the user  
  242.         if (mScreenBitmap == null) {  
  243.             return;  
  244.         }  
  245.   
  246.         // Optimizations  
  247.         mScreenBitmap.setHasAlpha(false);  
  248.         mScreenBitmap.prepareToDraw();  
  249.          
  250.   try {  
  251.    saveBitmap(mScreenBitmap);  
  252.   } catch (IOException e) {  
  253.    System.out.println(e.getMessage());  
  254.   }  
  255.  }  
  256.   
  257.  public void saveBitmap(Bitmap bitmap) throws IOException {  
  258.   String imageDate = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss")  
  259.     .format(new Date(System.currentTimeMillis()));  
  260.   File file = new File("/mnt/sdcard/Pictures/"+imageDate+".png");  
  261.   if(!file.exists()){  
  262.    file.createNewFile();  
  263.   }  
  264.   FileOutputStream out;  
  265.   try {  
  266.    out = new FileOutputStream(file);  
  267.    if (bitmap.compress(Bitmap.CompressFormat.PNG, 70, out)) {  
  268.     out.flush();  
  269.     out.close();  
  270.    }  
  271.   } catch (FileNotFoundException e) {  
  272.    e.printStackTrace();  
  273.   } catch (IOException e) {  
  274.    e.printStackTrace();  
  275.   }  
  276.  }  
  277. }  
  278.   
  279. PS:1、需要在AndroidManifest.xml中加入程式碼:android:sharedUserId="android.uid.system"  
  280.   
  281.          2、由於呼叫了@hide的API,所以編譯得時候請使用makefile編譯。或者通過在Eclipse中新增Jar檔案通過編譯。  
  282.   
  283.          3、此程式碼只在Android4.0中使用過,2.3的就沒去做測試了。  


4 利用TakeScreenShotService截圖

Android手機一般都自帶有手機螢幕截圖的功能:在手機任何介面(當然手機要是開機點亮狀態),通過按組合鍵,螢幕閃一下,然後咔嚓一聲,截圖的照片會儲存到當前手機的相簿中,真是一個不錯的功能!

以我手頭的測試手機為例,是同時按電源鍵+音量下鍵來實現截圖,蘋果手機則是電源鍵 + HOME鍵,小米手機是選單鍵+音量下鍵,而HTC一般是按住電源鍵再按左下角的“主頁”鍵。那麼Android原始碼中使用組合鍵是如何實現螢幕截圖功能呢?前段時間由於工作的原因仔細看了一下,這兩天不忙,便把相關的知識點串聯起來整理一下,分下面兩部分簡單分析下實現流程:

Android原始碼中對組合鍵的捕獲。

Android原始碼中對按鍵的捕獲位於檔案PhoneWindowManager.java(alps\frameworks\base\policy\src\com\android\internal\policy\impl)中,這個類處理所有的鍵盤輸入事件,其中函式interceptKeyBeforeQueueing()會對常用的按鍵做特殊處理。以我手頭的測試機為例,是同時按電源鍵和音量下鍵來截圖,那麼在這個函式中我們會看到這麼兩段程式碼:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
.......
 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事件)捕獲是否按了音量下鍵和電源鍵的,而且兩個地方都會進入函式interceptScreenshotChord()中,那麼接下來看看這個函式幹了什麼工作:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
    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());
            }
        }
    }

在這個函式中,用兩個布林變數判斷是否同時按了音量下鍵和電源鍵後,再計算兩個按鍵響應Down事件之間的時間差不超過150毫秒,也就認為是同時按了這兩個鍵後,算是真正的捕獲到螢幕截圖的組合鍵。

附言:檔案PhoneWindowManager.java類是攔截鍵盤訊息的處理類,在此類中還有對home鍵、返回鍵等好多按鍵的處理。

Android原始碼中呼叫螢幕截圖的介面。

捕獲到組合鍵後,我們再看看android原始碼中是如何呼叫螢幕截圖的函式介面。在上面的函式interceptScreenshotChord中我們看到用handler判斷長按組合鍵500毫秒之後,會進入如下函式:

1
2
3
4
5
    private final Runnable mScreenshotChordLongPress = new Runnable() {
        public void run() {
            takeScreenshot();
        }
    };

在這裡啟動了一個執行緒來完成截圖的功能,接著看函式takeScreenshot():

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
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);
            }
        }
    }

可以看到這個函式使用AIDL繫結了service服務到"com.android.systemui.screenshot.TakeScreenshotService",注意在service連線成功時,對message的msg.arg1和msg.arg2兩個引數的賦值。其中在mScreenshotTimeout中對服務service做了超時處理。接著我們找到實現這個服務service的類TakeScreenshotService,看看其實現的流程:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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()傳遞了三個引數,第一個是個runnable,第二和第三個是之前message傳遞的兩個引數msg.arg1和msg.arg2。最後我們看看這個函式takeScreenshot(),位於檔案GlobalScreenshot.java中(跟之前的函式重名但是檔案路徑不一樣):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
 /**
     * 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方法,發現已經是一個native方法了:

1
2
3
4
5
6
7
    /**
     * 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技術呼叫底層的程式碼,如果再往下走,會發現對映這這個jni函式在檔案android_view_Surface.cpp中,這個真的已經是底層c++語言了,統一呼叫的底層函式是:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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原始碼中通過組合鍵螢幕截圖的整個流程有個大體瞭解了,一般我們在改動中熟悉按鍵的捕獲原理,並且清楚呼叫的截圖函式介面即可,如果有興趣的,可以繼續探究更深的底層是如何實現的。

轉自:http://blog.csdn.net/woshinia/article/details/11520403

相關文章