Android相机开发(五): 触摸对焦,触摸测光,二指手势缩放
Android Camera Develop: touch to focus, touch to metering, double finger touch to zoom
概述
本篇在(四)的基础上继续对相机APP的功能进行增强。触摸对焦,就是在屏幕上点击某个点,相机就以此点内容进行对焦,保证此点最清晰;触摸测光,就是在屏幕上点击某个点,相机调整曝光亮度,保证此点亮度最为合适;二指手势缩放,就是通过手指在屏幕上的缩放,相机内容也随之进行缩放。上述三个功能也是目前相机APP较为常见的功能,我们接下来就进行实现。
触摸对焦
你要是仔细看过Camera.Parameters
的官方文档的话,大概见过setFocusAreas()
方法,就像字面意思一样,这个方法就是用来指定对焦区域的,而触摸对焦主要就是依靠这个方法实现。通过监听相机预览的触摸事件,获得手指触摸屏幕的坐标,然后通过setFocusAreas()
指定这个对焦区域,最后应用到相机就好了。
坐标转换
手指触摸屏幕的坐标并不能直接应用于setFocusAreas()
,因为相机会用到另一套坐标系,如下图所示(来自官方文档)
相机预览中心是(0, 0),左上角是(-1000, -1000),右下角是(1000, 1000)。其中蓝色的矩形就是一个对焦区域,相机以此区域进行对焦。这个坐标系可以让我们免于实际尺寸的困扰,还有一个好处就是这个坐标系不会受预览内容的旋转的影响,就是说只需要做一次坐标变换就好了。
在CameraPreview
中加入
private static Rect calculateTapArea(float x, float y, float coefficient, int width, int height) {
float focusAreaSize = 300;
int areaSize = Float.valueOf(focusAreaSize * coefficient).intValue();
int centerX = (int) (x / width * 2000 - 1000);
int centerY = (int) (y / height * 2000 - 1000);
int halfAreaSize = areaSize / 2;
RectF rectF = new RectF(clamp(centerX - halfAreaSize, -1000, 1000)
, clamp(centerY - halfAreaSize, -1000, 1000)
, clamp(centerX + halfAreaSize, -1000, 1000)
, clamp(centerY + halfAreaSize, -1000, 1000));
return new Rect(Math.round(rectF.left), Math.round(rectF.top), Math.round(rectF.right), Math.round(rectF.bottom));
}
private static int clamp(int x, int min, int max) {
if (x > max) {
return max;
}
if (x < min) {
return min;
}
return x;
}
calculateTapArea()
接收触摸点的坐标,返回转换后坐标介于[-1000, 1000]矩形。这是一个较为通用的代码,应该很容易看懂。
感谢评论区【11】同学的对calculateTapArea()
方法中的错误的指正!
设置对焦区域
在得到转换后的矩形后就可以直接通过setFocusAreas()
应用到相机了?实际没这么简单。直接这么做往往不能达到理想的效果,因为Android本身的问题以及设备的差异,在常用的对焦模式为continuous-picture
下,setFocusAreas()
可能会不工作。目前常用的解决办法是在setFocusAreas()
同时修改相机对焦模式为macro
等,待对焦完毕后,再将对焦模式修改为用户之前定义的。
在CameraPreview
中加入
private static void handleFocus(MotionEvent event, Camera camera) {
int viewWidth = getWidth();
int viewHeight = getHeight();
Rect focusRect = calculateTapArea(event.getX(), event.getY(), 1f, viewWidth, viewHeight);
camera.cancelAutoFocus();
Camera.Parameters params = camera.getParameters();
if (params.getMaxNumFocusAreas() > 0) {
List<Camera.Area> focusAreas = new ArrayList<>();
focusAreas.add(new Camera.Area(focusRect, 800));
params.setFocusAreas(focusAreas);
} else {
Log.i(TAG, "focus areas not supported");
}
final String currentFocusMode = params.getFocusMode();
params.setFocusMode(Camera.Parameters.FOCUS_MODE_MACRO);
camera.setParameters(params);
camera.autoFocus(new Camera.AutoFocusCallback() {
@Override
public void onAutoFocus(boolean success, Camera camera) {
Camera.Parameters params = camera.getParameters();
params.setFocusMode(currentFocusMode);
camera.setParameters(params);
}
});
}
前面大部分是设置曝光区域,很容易,getMaxNumFocusAreas()
用来判断相机是否支持设定手动对焦点,如果不支持就不用瞎折腾了;cancelAutoFocus()
是将相机的所以对焦完成后的回调函数都去掉,其实无关紧要。currentFocusMode
就是保存用户设置的对焦方式,然后将对焦方式修改为macro
,应用到相机,相机开始对焦。什么时候把对焦方式还原为用户设定的呢?当然是在相机对焦完成后,我们通过autoFocus()
设定一个回调函数,当相机对焦完成后就会调用这个回调函数,我们就可以在回调函数里设置将对焦方式修改回用户设定的,然后应用到相机。
捕获触摸事件
SurfaceView
就有onTouchEvent()
触摸事件,我们只需要将其重载实现自己想要的功能就好了。
在CameraPreview
中加入
public boolean onTouchEvent(MotionEvent event) {
if (event.getPointerCount() == 1) {
handleFocus(event, mCamera);
}
return true;
}
getPointerCount
获取手指数目,当只有一个手指时触发对焦,直接调用handleFocus()
就好了。
运行试试
现在APP就实现了触摸对焦了,运行试试吧。如下面两图就是分别以两本书的内容进行对焦,可以明显发现一本清晰一本模糊。
触摸测光
触摸测光与触摸对焦大同小异,一般来说我们希望在以指定点对焦的同时也以此点测光,调节亮度,只需要修改handleFocus()
就好了。
在handleFocus()
中加入
Rect meteringRect = calculateTapArea(event.getX(), event.getY(), 1.5f, viewWidth, viewHeight);
if (params.getMaxNumMeteringAreas() > 0) {
List<Camera.Area> meteringAreas = new ArrayList<>();
meteringAreas.add(new Camera.Area(meteringRect, 800));
params.setMeteringAreas(meteringAreas);
} else {
Log.i(TAG, "metering areas not supported");
}
getMaxNumMeteringAreas()
用来判断相机是否支持设定手动测光点,如果不支持就不用瞎折腾了。在DEMO中handleFocus()
的名字变为handleFocusMetering()
了,因为现在不止能够进行对焦了嘛。
运行试试
现在触摸屏幕会同时完成对焦和测光,运行试试吧。
二指手势缩放
这个听起来很难,但实际很容易。首先消除一个误解,当对相机进行缩放时,无论手指是在屏幕哪个地方缩放,实际都是以预览的中心进行缩放,因为缩放时相机的角度是没有变的。所以我们只需要知道用户两只手指是在放大还是缩小,然后通过setZoom()
指定缩放程度,应用到相机就好了。
手指间距
注意不同于触摸对焦,现在我们只需要知道手指是合拢还是张开,不需要知道手指的具体位置。怎么知道手指是合拢还是张开?可惜Android并没有提供这个方法,只会告诉我们有两个手指,还告诉手指的坐标;我们可以记下手指之间的间距,如果在手指移动时间距变大,那就是张开,否则就是合拢。
首先是计算手指间距,在CameraPreview
中加入
private static float getFingerSpacing(MotionEvent event) {
float x = event.getX(0) - event.getX(1);
float y = event.getY(0) - event.getY(1);
return (float) Math.sqrt(x * x + y * y);
}
从MotionEvent
中获取两个手指的坐标(提前保证一定有两个手指),然后计算距离,很简单。
设置缩放
判断手指合拢还是张开稍后再说,现在来看在知道是合拢还是张开后,怎么设置缩放。
对于相机来说,缩放程度是介于[0, getMaxZoom()
]之间的,不缩放时值为0,具体数值通过setZoom()
设置,应用到相机就能看到效果了。所以只需要在每次触发设置缩放时,根据是缩小还是放大,将缩放值减1或加1,并应用到相机。对于一次缩放手势,会多次触发设置缩放,这样就形成了一个连续的缩放过程,看起来就像过渡效果了。
在CameraPreview
中加入
private void handleZoom(boolean isZoomIn, Camera camera) {
Camera.Parameters params = camera.getParameters();
if (params.isZoomSupported()) {
int maxZoom = params.getMaxZoom();
int zoom = params.getZoom();
if (isZoomIn && zoom < maxZoom) {
zoom++;
} else if (zoom > 0) {
zoom--;
}
params.setZoom(zoom);
camera.setParameters(params);
} else {
Log.i(TAG, "zoom not supported");
}
}
isZoomSupported()
判断相机是否支持缩放,不支持就不用瞎折腾了。getMaxZoom()
获取最大缩放值,最小值为0不用获取;getZoom()
获取当前缩放值,如果是放大,且当前缩放值不超过最大值,则将当前缩放值加1;如果是缩小,且当前缩放值不小于0,则将当前缩放值减1。最后应用到相机,就完成了整个过程。
捕获二指缩放
先看代码,在CameraPreview
中加入
private float oldDist = 1f;
并将onTouchEvent()
修改为
public boolean onTouchEvent(MotionEvent event) {
if (event.getPointerCount() == 1) {
handleFocus(event, mCamera);
} else {
switch (event.getAction() & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_POINTER_DOWN:
oldDist = getFingerSpacing(event);
break;
case MotionEvent.ACTION_MOVE:
float newDist = getFingerSpacing(event);
if (newDist > oldDist) {
handleZoom(true, mCamera);
} else if (newDist < oldDist) {
handleZoom(false, mCamera);
}
oldDist = newDist;
break;
}
}
return true;
}
我们只看两指手势的部分。event.getAction() & MotionEvent.ACTION_MASK
获取手势类别;ACTION_POINTER_DOWN
即为两只手指触摸到屏幕,此时我们通过两只手指的坐标得到手指间距,记录到成员变量oldDist
中;ACTION_MOVE
即为手指在屏幕上移动,对应两只手指正在缩放,缩放过程中每次手指移动都会触发。此时记录新的手指间距为newDist
,并与oldDist
比较,确定缩放类型,调用handleZoom()
进行缩放;相机缩放完成后,将oldDist
赋值为newDist
,作为下一次触发ACTION_MOVE
的基准,这样完成缩放。
运行试试
现在在屏幕上用手指进行缩放,就会使相机预览缩放了,运行试试吧。如下面两图就是缩放前和缩放后
一点唠叨
上面我们实现了触摸对焦,触摸测光,二指手势缩放,看起来比较简单但也还是有许多细节问题值得深入探究。本篇美中不足的就是没有给触摸和手势加上动画,比如触摸时应该在屏幕上显示一个矩形指示,缩放时应该在屏幕上显示一个进度条指示缩放程度;鉴于加上这些内容需要更多代码和一些技巧,本篇没有实现,望自行查找(参考部分有个链接涉及到这个问题)。另外对于这些功能的实现也可以有不同的策略,我只是提出我认为最合适的方法,可能不是最好的。
DEMO
本文实现的相机APP源码都放在GitHub上,如果需要请点击zhantong/AndroidCamera-TouchToFocusMeteringZoom。
参考
- Camera | Android Developers
- Camera.Parameters | Android Developers
- Camera.Area | Android Developers
- View | Android Developers
- MotionEvent | Android Developers
- Android SurfaceView not responding to touch events - Stack Overflow
- Android setFocusArea and Auto Focus - Stack Overflow
- Android Camera preview zoom using double finger touch - Stack Overflow
- Android imageView Zoom-in and Zoom-Out - Stack Overflow
- android camera - Draw Rectangle on SurfaceView - Stack Overflow
请问博主 要使用前置摄像头 如何添加代码呀
博主,请问平均测光是如何实现?找了好多资料都没有介绍,有了解吗?
你好,不太理解你说的“平均测光”。如果你是说根据摄像头的平均光线强度调整曝光的话,不妨试试把测光的几个点均匀分布在画面平面上(部分Android设备是支持多个测光点的),然后综合算出数值,应用到Camera就好了。
这样写在相片的Exif显示,测光模式还是局部测光
这个就不太清楚咯,不过我猜是Android没有实现这个功能(或者是CMOS厂商没有实现),反正我看官方文档是没有提到这个概念的。
handleFocusMetering中的camera.setParameters(params) 在google pixel上实现不了,在gree上可以实现,网站找不到解决方案,想问下你怎么看?
不会又是Pixel硬件的坑吧...你看看支不支持meteringAreas这种参数,按照Google的尿性,是想让你用Camera2了。
还想请问下,拍摄的图片为什么会被压缩?每次拍摄完成后,点击相册预览功能,图片都有明显的压缩。
有压缩?这个应该是预览的时候没有专门处理图片的缩放,所以看起来会比较别扭吧。你可以用另外的APP打开图片或者下载下来到电脑上看试试,调用的API没有特殊处理的。
运行成功了,但是看起来很不稳定,连续拍照,或者拍照摄像之间来回切换,就会出问题,是不是线程问题没有处理好?
应该是对于Activity的销毁和重绘没有处理好,需要考虑清楚什么时候拿到camera和什么时候销毁camera instance的问题;为了简洁起见就只考虑了一般情况咯
从github上下载了,不能运行。
采用了handleFocusMeter的方法,发现没有效果,不知道问题出在那边,觉得camera.setParameters(params);没有起到作用
相机权限给了?抱歉这个问题没遇见过,一般来说确定相机权限没问题的话,setParameters()是不会出问题的呀...
测光功能这样实现不了的样子
int centerX = (int) (x / previewSize.width - 1000);
int centerY = (int) (y / previewSize.height - 1000);这两个坐标计算有问题,应该是除以屏幕的宽高之后再乘以2000
你好,非常感谢你的指正!!!
文章和代码均已作出修正!
我近期需要做直播中的手势缩放,是我操作引擎,引擎操作系统相机(牵涉适配问题).我操作引擎就得新启线程进行操作。然后就会遇到手指up了,还在放大的情况。 我自己调了一下参,action_move里面,加了阈值。这样可以做到up了,放缩停止。但是慢速缩放效果还ok,但是快速缩放,效果一般。没有系统相机流畅。 请问楼主有什么好方法吗?看到回复,谢谢。
MyLog.w(TAG, "0 newDist - oldDist =" + (newDist - oldDist)); mStreamer.handleZoom(true); oldDist = newDist; } else if (newDist < oldDist/* - 40*/) { MyLog.w(TAG, "1 oldDist - newDist =" + (oldDist - newDist)); mStreamer.handleZoom(false); oldDist = newDist; }if (newDist > oldDist /+ 40/) {
抱歉这么晚回复,因为不太清楚你说的线程问题,我只能说说我浅薄的看法了。
触发up了还在放大的问题,如果你不能解决新启动线程的问题的话,可以考虑看看官方文档关于触摸触发时间的更详细的介绍,换一种处理事件的方法,绕开这个问题。
关于加阈值我也在有些缩放实现里见过,自己也实践过,但最后还是采纳用比较大小的方法,因为阈值会遇到一些很古怪的bug,比如设备差异。加上你说的不流畅,考虑是不是有哪些处理代码占用了UI线程,照你这么来我比较倾向给switch那设个累加值,不要每次都触发handleZoom(),这样可以减小缩放调用次数,修改代码还能指定一次缩放的程度(我这里设的是1),不知这样是不是你想要的?
其实我还是觉得是线程的关系没有分配好,欢迎反馈!