在Android 开发过程中经常跟Camera 打交道,对Camera Orientation 的理解由开始的迷糊到精通,着实走了不少弯路。本文将对Android 系统Camera 相关经验做一个总结。
1. 手机的屏幕画面显示的自然方向:所谓自然方向就是人体手持手机时,视觉的习惯方向。例如,竖屏手机的自然方向一般都是垂直短边框向上,横屏手机的自然方向一般都是垂直长边框向上。这个自然方向是手机出厂时就定义好了的,相当于定义了一个坐标系,所有的旋转都要参照这个坐标系。
2. 本文提及的所有角度都是顺时针方向旋转,即以参照物为起点,目标物为终点,顺时针走过的角度即为目标物与参照物之间的夹角。
3. 当手机旋转时,display.getRotation() 返回的值为当前手机的空间位置与手机自然朝向之间的夹角。例如:将竖屛手机向右倾倒横放,display.getRotation() = 90;将竖屛手机向左倾倒横放,display.getRotation() = 270;将竖屛手机上下颠倒放置,display.getRotation() = 180。
4. 不管是横屏还是竖屛手机,Camera 的物理方向一般都是垂直于长边框向上。也就说对于竖屏手机,其Camera 物理方向与屏幕Display的自然方向的夹角为90度(顺时针)如下图一;对于横屏手机,其Camera 自然方向与屏幕Display的自然方向的夹角为0度,如下图二。
而对于竖屛手机,当手竖持手机拍照时,拍出的照片往往旋转了90度,这是因为在竖屛手机上,Camera 本身物理方向就与屏幕Display的自然方向有90度夹角,用手竖持手机时,Camera的方向是水平向右的,如下图一。因此要想没有90度旋转,需要设置 Camera.setDisplayOrientation(90) 来校正。
6. 如何计算当前Camera 物理位置与屏幕Display自然方向间的夹角?
算法的核心思想是:先假设手机为竖屛手机,然后按照竖屛手机的角度关系计算Camera 物理位置与屏幕Display自然方向间的夹角,最后根据长宽关系判断当前手机是否为竖屛,如果不是,补偿一个270度然后对360取模,非常巧妙。
public static int getOrientation(Context context) {
Display display = ((WindowManager) context.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();
int rotation = display.getRotation();
int orientation;
boolean expectPortrait;
switch (rotation) {
case Surface.ROTATION_0:
default:
orientation = 90;
expectPortrait = true;
break;
case Surface.ROTATION_90:
orientation = 0;
expectPortrait = false;
break;
case Surface.ROTATION_180:
orientation = 270;
expectPortrait = true;
break;
case Surface.ROTATION_270:
orientation = 180;
expectPortrait = false;
break;
}
boolean isPortrait = display.getHeight() > display.getWidth();
if (isPortrait != expectPortrait) {
orientation = (orientation + 270) % 360;
}
return orientation;
}
7.如何为Surface View 设置一个最佳的尺寸?
private Size calculateBestPreviewSizeAndAdjustLayout(Camera camera, Rect surfaceSize, int orientation, int minHeight, int maxHeight) {
SizeAndRatio optimalSizeAndRatio = null; // output
double targetRatio = (double)surfaceSize.right/surfaceSize.bottom;
if (orientation % 180 == 90) {
// We are in portrait
targetRatio = 1/targetRatio;
m_updatedCameraFrameLayoutSize = new Point(surfaceSize.bottom,surfaceSize.right);
} else {
m_updatedCameraFrameLayoutSize = new Point(surfaceSize.right,surfaceSize.bottom);
}
boolean allowedResize = android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.ICE_CREAM_SANDWICH && orientation != 270;
List<Size> supportedPreviewSizes = camera.getParameters().getSupportedPreviewSizes();
// Collect sub-list that meets height requirements
List<SizeAndRatio> cameraSizesAndRatios = new ArrayList<SizeAndRatio>();
for (Size s : supportedPreviewSizes) {
if (s.height < minHeight || s.height > maxHeight) {
continue; // Filter out too small or too large sizes
}
double ratio = ((double) s.width) / s.height;
SizeAndRatio sr = new SizeAndRatio(s, ratio);
cameraSizesAndRatios.add(sr);
}
// Error, no available sizes
if (cameraSizesAndRatios.isEmpty()) {
return null;
}
// Sort the list by height descending
Collections.sort(cameraSizesAndRatios, new Comparator<SizeAndRatio>() {
@Override
public int compare(SizeAndRatio a_lhs, SizeAndRatio a_rhs) {
return (a_rhs.size.height) - (a_lhs.size.height);
}
});
// First try to find an exact screen aspect ratio match
for (SizeAndRatio sr : cameraSizesAndRatios) {
if (Math.abs(sr.ratio - targetRatio) < EPSILON_TOLERANCE) {
optimalSizeAndRatio = sr;
break;
}
}
if (optimalSizeAndRatio != null) {
return optimalSizeAndRatio.size;
}
// If we allow layout resizing, try to find 16:9 or 4:3 ratio
if (allowedResize) {
// Try to find a 16:9 ratio
for (SizeAndRatio sr : cameraSizesAndRatios) {
if (Math.abs(sr.ratio - RATIO16_9) < EPSILON_TOLERANCE) {
optimalSizeAndRatio = sr;
break;
}
}
if (optimalSizeAndRatio != null) {<p class="p1"> // Notify that the screen layout should change</p><p class="p2"> <span >m_updatedCameraFrameLayoutSize</span> = adjustSurfaceSize(<span >mActivity</span>, optimalSizeAndRatio.<span >ratio</span>, orientation);</p> return optimalSizeAndRatio.size;
}
// Try to find a 4:3 ratio
for (SizeAndRatio sr : cameraSizesAndRatios) {
if (Math.abs(sr.ratio - RATIO4_3) < EPSILON_TOLERANCE) {
optimalSizeAndRatio = sr;
break;
}
}
if (optimalSizeAndRatio != null) {<p class="p1"><span > </span>// Notify that the screen layout should change</p><p class="p2"> <span class="s2">m_updatedCameraFrameLayoutSize</span> = adjustSurfaceSize(<span class="s2">mActivity</span>, optimalSizeAndRatio.<span class="s2">ratio</span>, orientation);</p> return optimalSizeAndRatio.size;
}
}
// Now just use preview size with smallest ratio difference
double minRatioDifference = Double.MAX_VALUE;
for (SizeAndRatio sr : cameraSizesAndRatios) {
double ratioDifference = Math.abs(sr.ratio - targetRatio);
if (ratioDifference < minRatioDifference) {
minRatioDifference = ratioDifference;
optimalSizeAndRatio = sr;
}
}<p class="p1"> </p><p class="p1"><span > if</span> (allowedResize) {</p><p class="p1"> <span class="s2">m_updatedCameraFrameLayoutSize</span> = adjustSurfaceSize(<span class="s2">mActivity</span>, optimalSizeAndRatio.<span class="s2">ratio</span>, orientation);</p><p class="p1"> }</p>
return optimalSizeAndRatio.size;
}
public Point adjustSurfaceSize(FSECameraActivity activity, double a_aspectRatio, int a_orientation) {
boolean isPortrait = a_orientation % 180 == 90;
// Collect the width, height, and ratio of the screen
Display display = activity.getWindowManager().getDefaultDisplay();
int windowWidth, windowHeight;
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.HONEYCOMB_MR2) {
Point size = new Point();
display.getSize(size);
windowWidth = size.x;
windowHeight = size.y;
} else {
// Old APIs
windowWidth = display.getWidth();
windowHeight = display.getHeight();
}
int leftMargin = 0; // This may need to be adjusted for increasing width in portrait
double currentRatio = (double)windowWidth / (double)windowHeight;
// Adjust the ratios
if (currentRatio < 1 && a_aspectRatio > 1 || currentRatio > 1 && a_aspectRatio < 1) {
a_aspectRatio = 1.0/a_aspectRatio;
}
// Adjust the necessary dimension
if (currentRatio > a_aspectRatio) {
// If current ratio is larger, we need to increase the smaller dimension (height)
windowHeight = (int) (windowWidth / a_aspectRatio);
} else if (currentRatio < a_aspectRatio) {
// If current ratio is smaller, we need to increase the larger dimension (width)
int oldWindowWidth = windowWidth;
windowWidth = (int) (windowHeight * a_aspectRatio);
if (isPortrait) {
// We also need to fix the Left Margin if in portrait
leftMargin = oldWindowWidth - windowWidth;
}
} else if (currentRatio == a_aspectRatio) {
// No adjustment needed
}
// Update layout parameters
final LayoutParams lp = (FrameLayout.LayoutParams) mPreviewLayout.getLayoutParams();
lp.width = windowWidth;
lp.height = windowHeight;
lp.topMargin = 0;
lp.leftMargin = leftMargin;
mCameraScreenMarginLeft = leftMargin;
mPreviewLayout.setLayoutParams(lp);
// Return a point where X corresponds to Camera Width, and Y corresponds to Camera Height.
return isPortrait ? new Point(windowHeight, windowWidth) : new Point(windowWidth, windowHeight);
}
8. 如何将Camera 捕景框上的点坐标转化为手机屏幕上的坐标?
/**
* Translate a point returned by <span style="font-family: Arial, Helvetica, sans-serif;">camera sensor </span>into a point on the camera surface preview
* @param point input Point returned from <span style="font-family: Arial, Helvetica, sans-serif;">camera sensor</span>
* @return PointF
*/
public PointF translatePoint(PointF point) {
switch (mOrientation) {
case 0: // landscape right
return new PointF(point.x*m_updatedCameraFrameLayoutSize.x/mCameraPreviewSize.width,
point.y*m_updatedCameraFrameLayoutSize.y/mCameraPreviewSize.height);
case 180: // landscape left
return new PointF((mCameraPreviewSize.width-point.x)*m_updatedCameraFrameLayoutSize.x/mCameraPreviewSize.width,
(mCameraPreviewSize.height-point.y)*m_updatedCameraFrameLayoutSize.y/mCameraPreviewSize.height);
case 270: // upside down portrait
return new PointF(point.y*m_updatedCameraFrameLayoutSize.y/mCameraPreviewSize.height,
(mCameraPreviewSize.width-point.x)*m_updatedCameraFrameLayoutSize.x/mCameraPreviewSize.width);
default: // 90, normal portrait
return new PointF((mCameraPreviewSize.height-point.y)*m_updatedCameraFrameLayoutSize.y/mCameraPreviewSize.height,
point.x*m_updatedCameraFrameLayoutSize.x/mCameraPreviewSize.width);
}
}