转-Android应用截图两种方法

 

http://blog.csdn.net/jokers_i/article/details/39549633

在Android应用开发过程中,可能会遇到需要对整个界面或者某一部分进行截图的需求。Android中对View的截图也有很多中方式:

  1. 使用DrawingCache
  2. 直接调用View.draw
DrawingCache方法:
如果使用DrawingCache,则对要截图的View有一个要求:View本身已经显示在界面上。如果View没有添加到界面上或者没有显示(绘制)过,则buildDrawingCache会失败。这种方式比较适合对应用界面或者某一部分的截图。步骤很简单:
  1. view.setDrawingCacheEnabled(true);
  2. view.buildDrawingCache();  //启用DrawingCache并创建位图
  3. Bitmap bitmap = Bitmap.createBitmap(view.getDrawingCache()); //创建一个DrawingCache的拷贝,因为DrawingCache得到的位图在禁用后会被回收
  4. view.setDrawingCacheEnabled(false);  //禁用DrawingCahce否则会影响性能
  5. destroyDrawingCache();

View.draw方法:

如果需要截图的View并没有添加到界面上,可能是通过java代码创建的或者inflate创建的,此时调用DrawingCache方法是获取不到位图的。因为View在添加到容器中之前并没有得到实际的大小(如果LayoutWidth是MatchParent,它还没有Parent…),所以首先需要指定View的大小:
  1. private void layoutView(View v, int width, int height) {
  2.     // validate view.width and view.height
  3.     v.layout(00, width, height);
  4.     int measuredWidth = View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY);
  5.     int measuredHeight = View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY);
  6.     // validate view.measurewidth and view.measureheight
  7.     v.measure(measuredWidth, measuredHeight);
  8.     v.layout(00, v.getMeasuredWidth(), v.getMeasuredHeight());i
  9. }

然后View和其内部的子View都具有了实际大小,也就是完成了布局,相当与添加到了界面上。接着就可以创建位图并在上面绘制了:

  1. int viewWidth = webView.getMeasuredWidth();
  2. int viewHeight = webView.getMeasuredHeight();
  3. if (viewWidth > 0 && viewHeight > 0) {
  4.     b = Bitmap.createBitmap(viewWidth, viewHeight, Config.ARGB_8888);
  5.     Canvas cvs = new Canvas(b);
  6.     webView.draw(cvs);
  7. }

对于WebView的截图有一点特殊,网页内容并不能在布局完成后立即渲染出来,因为WebView大小的变化就相当与桌面浏览器窗口大小的变化,Webkit需要根据窗口大小重新渲染所有的内容,这最多大概需要300ms的时间(对于不同性能的设备、网页复杂程度和Webkit版本可能不同)。如果创建后台的WebView需要截图的话,应该在创建时就对其进行布局操作,这样加载完成后大部分就已经渲染完毕了(除非有异步的js处理)。

 

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进行截屏

  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.   for (int i = 0; i < 10; i++) {
  12.    Date date=new Date();
  13.    SimpleDateFormat df=new SimpleDateFormat(“MM-dd-HH-mm-ss”);
  14.    String nowTime = df.format(date);
  15.    screenshot.getScreenShot(device, “Robotium” + nowTime);
  16.    try {
  17.     Thread.sleep(1000);
  18.    } catch (InterruptedException e) {
  19.     // TODO Auto-generated catch block
  20.     e.printStackTrace();
  21.    }
  22.   }
  23.  }
  24.  public void getScreenShot(IDevice device,String filename) {
  25.   RawImage rawScreen = null;
  26.   try {
  27.    rawScreen = device.getScreenshot();
  28.   } catch (TimeoutException e) {
  29.    // TODO Auto-generated catch block
  30.    e.printStackTrace();
  31.   } catch (AdbCommandRejectedException e) {
  32.    // TODO Auto-generated catch block
  33.    e.printStackTrace();
  34.   } catch (IOException e) {
  35.    // TODO Auto-generated catch block
  36.    e.printStackTrace();
  37.   }
  38.   if (rawScreen != null) {
  39.    Boolean landscape = false;
  40.    int width2 = landscape ? rawScreen.height : rawScreen.width;
  41.    int height2 = landscape ? rawScreen.width : rawScreen.height;
  42.    if (image == null) {
  43.     image = new BufferedImage(width2, height2,
  44.       BufferedImage.TYPE_INT_RGB);
  45.    } else {
  46.     if (image.getHeight() != height2 || image.getWidth() != width2) {
  47.      image = new BufferedImage(width2, height2,
  48.        BufferedImage.TYPE_INT_RGB);
  49.     }
  50.    }
  51.    int index = 0;
  52.    int indexInc = rawScreen.bpp >> 3;
  53.    for (int y = 0; y < rawScreen.height; y++) {
  54.     for (int x = 0; x < rawScreen.width; x++, index += indexInc) {
  55.      int value = rawScreen.getARGB(index);
  56.      if (landscape)
  57.       image.setRGB(y, rawScreen.width – x – 1, value);
  58.      else
  59.       image.setRGB(x, y, value);
  60.     }
  61.    }
  62.    try {
  63.     ImageIO.write((RenderedImage) image, “PNG”new File(“D:/”
  64.       + filename + “.jpg”));
  65.    } catch (IOException e) {
  66.     // TODO Auto-generated catch block
  67.     e.printStackTrace();
  68.    }
  69.   }
  70.  }
  71.  /**
  72.   * 获取得到device对象
  73.   * @return
  74.   */
  75.  private IDevice getDevice(){
  76.   IDevice device;
  77.   AndroidDebugBridge bridge = AndroidDebugBridge
  78.     .createBridge(“adb”true);//如果代码有问题请查看API,修改此处的参数值试一下
  79.   waitDevicesList(bridge);
  80.   IDevice devices[] = bridge.getDevices();
  81.   device = devices[0];
  82.   return device;
  83.  }
  84.  /**
  85.   * 等待查找device
  86.   * @param bridge
  87.   */
  88.  private void waitDevicesList(AndroidDebugBridge bridge) {
  89.   int count = 0;
  90.   while (bridge.hasInitialDeviceList() == false) {
  91.    try {
  92.     Thread.sleep(500);
  93.     count++;
  94.    } catch (InterruptedException e) {
  95.    }
  96.    if (count > 240) {
  97.     System.err.print(“等待获取设备超时”);
  98.     break;
  99.    }
  100.   }
  101.  }

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)

  1. 首先是直接移植SystemUI的代码,实现截图效果,这部分的代码就不贴出来了,直接去下载代码吧, 关键的代码没有几句,最最主要的是:Surface.screenshot(),请看代码吧。
  2. [java]
  3. <SPAN style=“FONT-SIZE: 16px”>package org.winplus.ss;
  4. import java.io.File;
  5. import java.io.FileNotFoundException;
  6. import java.io.FileOutputStream;
  7. import java.io.IOException;
  8. import java.text.SimpleDateFormat;
  9. import java.util.Date;
  10. import android.app.Activity;
  11. import android.content.Context;
  12. import android.graphics.Bitmap;
  13. import android.graphics.Canvas;
  14. import android.graphics.Matrix;
  15. import android.os.Bundle;
  16. import android.util.DisplayMetrics;
  17. import android.util.Log;
  18. import android.view.Display;
  19. import android.view.Surface;
  20. import android.view.WindowManager;
  21. import android.os.SystemProperties;
  22. public class SimpleScreenshotActivity extends Activity {
  23.     private Display mDisplay;
  24.     private WindowManager mWindowManager;
  25.     private DisplayMetrics mDisplayMetrics;
  26.     private Bitmap mScreenBitmap;
  27.     private Matrix mDisplayMatrix;
  28.     @Override
  29.     public void onCreate(Bundle savedInstanceState) {
  30.         super.onCreate(savedInstanceState);
  31.         setContentView(R.layout.main);
  32.         new Thread(new Runnable() {
  33.             @Override
  34.             public void run() {
  35.                 takeScreenshot();
  36.             }
  37.         }).start();
  38.     }
  39.     private float getDegreesForRotation(int value) {
  40.         switch (value) {
  41.         case Surface.ROTATION_90:
  42.             return 360f – 90f;
  43.         case Surface.ROTATION_180:
  44.             return 360f – 180f;
  45.         case Surface.ROTATION_270:
  46.             return 360f – 270f;
  47.         }
  48.         return 0f;
  49.     }
  50.     private void takeScreenshot() {
  51.         mWindowManager = (WindowManager) getSystemService(Context.WINDOW_SERVICE);
  52.         mDisplay = mWindowManager.getDefaultDisplay();
  53.         mDisplayMetrics = new DisplayMetrics();
  54.         mDisplay.getRealMetrics(mDisplayMetrics);
  55.         mDisplayMatrix = new Matrix();
  56.         float[] dims = { mDisplayMetrics.widthPixels,
  57.                 mDisplayMetrics.heightPixels };
  58.         int value = mDisplay.getRotation();
  59.         String hwRotation = SystemProperties.get(“ro.sf.hwrotation”“0”);
  60.         if (hwRotation.equals(“270”) || hwRotation.equals(“90”)) {
  61.             value = (value + 3) % 4;
  62.         }
  63.         float degrees = getDegreesForRotation(value);
  64.         boolean requiresRotation = (degrees > 0);
  65.         if (requiresRotation) {
  66.             // Get the dimensions of the device in its native orientation  
  67.             mDisplayMatrix.reset();
  68.             mDisplayMatrix.preRotate(-degrees);
  69.             mDisplayMatrix.mapPoints(dims);
  70.             dims[0] = Math.abs(dims[0]);
  71.             dims[1] = Math.abs(dims[1]);
  72.         }
  73.         mScreenBitmap = Surface.screenshot((int) dims[0], (int) dims[1]);
  74.         if (requiresRotation) {
  75.             // Rotate the screenshot to the current orientation  
  76.             Bitmap ss = Bitmap.createBitmap(mDisplayMetrics.widthPixels,
  77.                     mDisplayMetrics.heightPixels, Bitmap.Config.ARGB_8888);
  78.             Canvas c = new Canvas(ss);
  79.             c.translate(ss.getWidth() / 2, ss.getHeight() / 2);
  80.             c.rotate(degrees);
  81.             c.translate(-dims[0] / 2, -dims[1] / 2);
  82.             c.drawBitmap(mScreenBitmap, 00null);
  83.             c.setBitmap(null);
  84.             mScreenBitmap = ss;
  85.         }
  86.         // If we couldn’t take the screenshot, notify the user  
  87.         if (mScreenBitmap == null) {
  88.             return;
  89.         }
  90.         // Optimizations  
  91.         mScreenBitmap.setHasAlpha(false);
  92.         mScreenBitmap.prepareToDraw();
  93.         try {
  94.             saveBitmap(mScreenBitmap);
  95.         } catch (IOException e) {
  96.             System.out.println(e.getMessage());
  97.         }
  98.     }
  99.     public void saveBitmap(Bitmap bitmap) throws IOException {
  100.         String imageDate = new SimpleDateFormat(“yyyy-MM-dd-HH-mm-ss”)
  101.                 .format(new Date(System.currentTimeMillis()));
  102.         File file = new File(“/mnt/sdcard/Pictures/”+imageDate+“.png”);
  103.         if(!file.exists()){
  104.             file.createNewFile();
  105.         }
  106.         FileOutputStream out;
  107.         try {
  108.             out = new FileOutputStream(file);
  109.             if (bitmap.compress(Bitmap.CompressFormat.PNG, 70, out)) {
  110.                 out.flush();
  111.                 out.close();
  112.             }
  113.         } catch (FileNotFoundException e) {
  114.             e.printStackTrace();
  115.         } catch (IOException e) {
  116.             e.printStackTrace();
  117.         }
  118.     }
  119. }
  120. </SPAN>
  121. package org.winplus.ss;
  122. import java.io.File;
  123. import java.io.FileNotFoundException;
  124. import java.io.FileOutputStream;
  125. import java.io.IOException;
  126. import java.text.SimpleDateFormat;
  127. import java.util.Date;
  128. import android.app.Activity;
  129. import android.content.Context;
  130. import android.graphics.Bitmap;
  131. import android.graphics.Canvas;
  132. import android.graphics.Matrix;
  133. import android.os.Bundle;
  134. import android.util.DisplayMetrics;
  135. import android.util.Log;
  136. import android.view.Display;
  137. import android.view.Surface;
  138. import android.view.WindowManager;
  139. import android.os.SystemProperties;
  140. public class SimpleScreenshotActivity extends Activity {
  141.  private Display mDisplay;
  142.  private WindowManager mWindowManager;
  143.  private DisplayMetrics mDisplayMetrics;
  144.  private Bitmap mScreenBitmap;
  145.  private Matrix mDisplayMatrix;
  146.  @Override
  147.  public void onCreate(Bundle savedInstanceState) {
  148.   super.onCreate(savedInstanceState);
  149.   setContentView(R.layout.main);
  150.   new Thread(new Runnable() {
  151.    @Override
  152.    public void run() {
  153.     takeScreenshot();
  154.    }
  155.   }).start();
  156.  }
  157.  private float getDegreesForRotation(int value) {
  158.   switch (value) {
  159.   case Surface.ROTATION_90:
  160.    return 360f – 90f;
  161.   case Surface.ROTATION_180:
  162.    return 360f – 180f;
  163.   case Surface.ROTATION_270:
  164.    return 360f – 270f;
  165.   }
  166.   return 0f;
  167.  }
  168.  private void takeScreenshot() {
  169.   mWindowManager = (WindowManager) getSystemService(Context.WINDOW_SERVICE);
  170.   mDisplay = mWindowManager.getDefaultDisplay();
  171.   mDisplayMetrics = new DisplayMetrics();
  172.   mDisplay.getRealMetrics(mDisplayMetrics);
  173.   mDisplayMatrix = new Matrix();
  174.   float[] dims = { mDisplayMetrics.widthPixels,
  175.     mDisplayMetrics.heightPixels };
  176.   int value = mDisplay.getRotation();
  177.   String hwRotation = SystemProperties.get(“ro.sf.hwrotation”“0”);
  178.   if (hwRotation.equals(“270”) || hwRotation.equals(“90”)) {
  179.    value = (value + 3) % 4;
  180.   }
  181.   float degrees = getDegreesForRotation(value);
  182.   boolean requiresRotation = (degrees > 0);
  183.   if (requiresRotation) {
  184.    // Get the dimensions of the device in its native orientation
  185.    mDisplayMatrix.reset();
  186.    mDisplayMatrix.preRotate(-degrees);
  187.    mDisplayMatrix.mapPoints(dims);
  188.    dims[0] = Math.abs(dims[0]);
  189.    dims[1] = Math.abs(dims[1]);
  190.   }
  191.   mScreenBitmap = Surface.screenshot((int) dims[0], (int) dims[1]);
  192.   if (requiresRotation) {
  193.             // Rotate the screenshot to the current orientation
  194.             Bitmap ss = Bitmap.createBitmap(mDisplayMetrics.widthPixels,
  195.                     mDisplayMetrics.heightPixels, Bitmap.Config.ARGB_8888);
  196.             Canvas c = new Canvas(ss);
  197.             c.translate(ss.getWidth() / 2, ss.getHeight() / 2);
  198.             c.rotate(degrees);
  199.             c.translate(-dims[0] / 2, -dims[1] / 2);
  200.             c.drawBitmap(mScreenBitmap, 00null);
  201.             c.setBitmap(null);
  202.             mScreenBitmap = ss;
  203.         }
  204.         // If we couldn’t take the screenshot, notify the user
  205.         if (mScreenBitmap == null) {
  206.             return;
  207.         }
  208.         // Optimizations
  209.         mScreenBitmap.setHasAlpha(false);
  210.         mScreenBitmap.prepareToDraw();
  211.   try {
  212.    saveBitmap(mScreenBitmap);
  213.   } catch (IOException e) {
  214.    System.out.println(e.getMessage());
  215.   }
  216.  }
  217.  public void saveBitmap(Bitmap bitmap) throws IOException {
  218.   String imageDate = new SimpleDateFormat(“yyyy-MM-dd-HH-mm-ss”)
  219.     .format(new Date(System.currentTimeMillis()));
  220.   File file = new File(“/mnt/sdcard/Pictures/”+imageDate+“.png”);
  221.   if(!file.exists()){
  222.    file.createNewFile();
  223.   }
  224.   FileOutputStream out;
  225.   try {
  226.    out = new FileOutputStream(file);
  227.    if (bitmap.compress(Bitmap.CompressFormat.PNG, 70, out)) {
  228.     out.flush();
  229.     out.close();
  230.    }
  231.   } catch (FileNotFoundException e) {
  232.    e.printStackTrace();
  233.   } catch (IOException e) {
  234.    e.printStackTrace();
  235.   }
  236.  }
  237. }
  238. PS:1、需要在AndroidManifest.xml中加入代码:android:sharedUserId=“android.uid.system”
  239.          2、由于调用了@hide的API,所以编译得时候请使用makefile编译。或者通过在Eclipse中添加Jar文件通过编译。
  240.          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源码中通过组合键屏幕截图的整个流程有个大体了解了,一般我们在改动中熟悉按键的捕获原理,并且清楚调用的截屏函数接口即可,如果有兴趣的,可以继续探究更深的底层是如何实现的。

应用