当前位置: 首页 > article >正文

安卓触摸对焦

1. 相机坐标说明

触摸对焦需要通过setFocusAreas()设置对焦区域,而该方法的参数的坐标,与屏幕坐标并不相同,需要做一个转换。

Camera(旧版相机API)来说,相机的坐标区域是一个2000*2000,原点在中心区域,该区域与相机预览画面的边界重合,如下图所示:

相机坐标与屏幕坐标

谷歌的文档对getFocusAreas()方法的描述如下(文档连接):

Gets the current focus areas. Camera driver uses the areas to decide focus.

Before using this API or setFocusAreas(java.util.List), apps should call getMaxNumFocusAreas() to know the maximum
number of focus areas first. If the value is 0, focus area is not supported.

Each focus area is a rectangle with specified weight. The direction is relative to the sensor orientation, that is,
what the sensor sees. The direction is not affected by the rotation or mirroring of Camera.setDisplayOrientation(int).
Coordinates of the rectangle range from -1000 to 1000. (-1000, -1000) is the upper left point. (1000, 1000) is the lower
right point. The width and height of focus areas cannot be 0 or negative.

The weight must range from 1 to 1000. The weight should be interpreted as a per-pixel weight - all pixels in the area
have the specified weight. This means a small area with the same weight as a larger area will have less influence on the
focusing than the larger area. Focus areas can partially overlap and the driver will add the weights in the overlap
region.

A special case of a null focus area list means the driver is free to select focus targets as it wants. For example,
the driver may use more signals to select focus areas and change them dynamically. Apps can set the focus area list to
null if they want the driver to completely control focusing.

Focus areas are relative to the current field of view (getZoom()). No matter what the zoom level is, (-1000,-1000)
represents the top of the currently visible camera frame. The focus area cannot be set to be outside the current field
of view, even when using zoom.

Focus area only has effect if the current focus mode is FOCUS_MODE_AUTO, FOCUS_MODE_MACRO,
FOCUS_MODE_CONTINUOUS_VIDEO, or FOCUS_MODE_CONTINUOUS_PICTURE.

同时,设置测光区域setMeteringAreas()的方法参数的坐标,跟对焦区域的坐标是一样的,文档对getMeteringAreas()的描述如下(文档连接):

Gets the current metering areas. Camera driver uses these areas to decide exposure.

Before using this API or setMeteringAreas(java.util.List), apps should call getMaxNumMeteringAreas() to know the
maximum number of metering areas first. If the value is 0, metering area is not supported.

Each metering area is a rectangle with specified weight. The direction is relative to the sensor orientation, that is,
what the sensor sees. The direction is not affected by the rotation or mirroring of Camera.setDisplayOrientation(int).
Coordinates of the rectangle range from -1000 to 1000. (-1000, -1000) is the upper left point. (1000, 1000) is the lower
right point. The width and height of metering areas cannot be 0 or negative.

The weight must range from 1 to 1000, and represents a weight for every pixel in the area. This means that a large
metering area with the same weight as a smaller area will have more effect in the metering result. Metering areas can
partially overlap and the driver will add the weights in the overlap region.

A special case of a null metering area list means the driver is free to meter as it chooses. For example, the driver
may use more signals to select metering areas and change them dynamically. Apps can set the metering area list to null
if they want the driver to completely control metering.

Metering areas are relative to the current field of view (getZoom()). No matter what the zoom level is, (-1000,-1000)
represents the top of the currently visible camera frame. The metering area cannot be set to be outside the current
field of view, even when using zoom.

No matter what metering areas are, the final exposure are compensated by setExposureCompensation(int).

2. 坐标转换

根据前面对相机坐标的说明,对坐标进行转换就很简单了。

我采用的方法是:计算坐标到左上角的x、y方向的距离,与x、y轴长度的百分比,然后乘以新坐标的x、y轴长度,再根据原点与左上角的位置计算坐标。核心代码如下:

transX = (x / xMax) * 2000 - 1000
trnasY = (y / yMax) * 2000 - 1000

如果设置了预览画面的旋转角度(camera.setDisplayOrientation(orientation)),由于触摸事件中MotionEvent对象传递的是屏幕坐标,所以需要旋转。为了方便,选择在相机坐标下旋转,代码如下:

Matrix matrix = new Matrix();
matrix.setRotate(orientation);
matrix.mapRect(rectF); // rectF是相机坐标下的矩形

完整的工具类CameraAreaUtils.java代码如下:

package com.example.study.utils;

import android.graphics.Matrix;
import android.graphics.Rect;
import android.graphics.RectF;

import com.example.study.entities.CameraAreaEntity;

public class CameraAreaUtils {
    /**
     * 相机坐标区域是2000*2000,坐标原点在最中心
     */
    private static final int CAMERA_COORDINATE_SIZE = 2000;

    /**
     * 原点到边界的距离
     */
    private static final int CAMERA_COORDINATE_HALF_SIZE = CAMERA_COORDINATE_SIZE >> 1;

    /**
     * 根据中心点、边框边长、边框长度系数创建矩形区域
     *
     * @param areaEntity 矩形区域信息
     * @return 矩形
     */
    public static Rect createRect(CameraAreaEntity areaEntity) {
        float x = areaEntity.getX();
        float y = areaEntity.getY();
        int previewWidth = areaEntity.getPreviewWidth();
        int previewHeight = areaEntity.getPreviewHeight();
        int sideSize = (int) (areaEntity.getAreaSide() * areaEntity.getCoefficient());
        int left = clamp(x - sideSize / 2, 0, previewWidth);
        int top = clamp(y - sideSize / 2, 0, previewHeight);
        int right = clamp(x + sideSize / 2, 0, previewWidth);
        int bottom = clamp(y + sideSize / 2, 0, previewHeight);
        if (right - left < sideSize) {
            if (left == 0) {
                right = Math.min(sideSize, previewWidth);
            }
            if (right == previewWidth) {
                left = Math.max(previewWidth - sideSize, 0);
            }
        }
        if (bottom - top < sideSize) {
            if (top == 0) {
                bottom = Math.min(sideSize, previewHeight);
            }
            if (bottom == previewHeight) {
                top = Math.max(previewHeight - sideSize, 0);
            }
        }
        return new Rect(left, top, right, bottom);
    }

    /**
     * 防止坐标越界
     *
     * @param position 坐标
     * @param min      最小值
     * @param max      最大值
     * @return 坐标
     */
    private static int clamp(float position, int min, int max) {
        int pos = Math.round(position);
        return pos < min ? min : pos > max ? max : pos;
    }

    /**
     * 屏幕矩形区域转为相机矩形区域
     *
     * @param areaEntity 矩形区域信息
     * @return 相机矩形区域
     */
    public static Rect transToCamera(CameraAreaEntity areaEntity) {
        Rect rect = createRect(areaEntity);
        return rectFToRect(transToCamera(new RectF(rect), areaEntity));
    }

    /**
     * 屏幕矩形区域转为相机矩形区域
     *
     * @param rectF      屏幕矩形区域
     * @param areaEntity 矩形区域信息
     * @return 相机矩形区域
     */
    public static RectF transToCamera(RectF rectF, CameraAreaEntity areaEntity) {
        int previewWidth = areaEntity.getPreviewWidth();
        int previewHeight = areaEntity.getPreviewHeight();
        RectF cameraRectF = new RectF(transPositionToCameraCoordinate(rectF.left, previewWidth),
                transPositionToCameraCoordinate(rectF.top, previewHeight),
                transPositionToCameraCoordinate(rectF.right, previewWidth),
                transPositionToCameraCoordinate(rectF.bottom, previewHeight));
        // 预览画面如果有旋转,映射到相机坐标后,需要把旋转取消,才能对应真正的相机坐标
        Matrix matrix = new Matrix();
        matrix.setRotate(-1 * areaEntity.getOrientation());
        matrix.mapRect(cameraRectF);
        return cameraRectF;
    }

    /**
     * 将屏幕坐标转换为相机坐标
     *
     * @param position 屏幕坐标某个方向的坐标值(如x)
     * @param srcSize  屏幕坐标对应方向的尺寸(如width)
     * @return 相机坐标下该方向的坐标值
     */
    private static float transPositionToCameraCoordinate(float position, int srcSize) {
        return (position / srcSize) * CAMERA_COORDINATE_SIZE - CAMERA_COORDINATE_HALF_SIZE;
    }

    /**
     * 相机矩形区域转为屏幕矩形区域
     *
     * @param rect       相机矩形区域
     * @param areaEntity 矩形区域信息
     * @return 屏幕矩形区域
     */
    public static Rect transToScreen(Rect rect, CameraAreaEntity areaEntity) {
        return rectFToRect(transToScreen(new RectF(rect), areaEntity));
    }

    /**
     * 相机矩形区域转为屏幕矩形区域
     *
     * @param rectF      相机矩形区域
     * @param areaEntity 矩形区域信息
     * @return 屏幕矩形区域
     */
    public static RectF transToScreen(RectF rectF, CameraAreaEntity areaEntity) {
        int previewWidth = areaEntity.getPreviewWidth();
        int previewHeight = areaEntity.getPreviewHeight();
        // 先把相机坐标按预览的旋转角度进行旋转
        Matrix matrix = new Matrix();
        matrix.setRotate(areaEntity.getOrientation());
        matrix.mapRect(rectF);
        RectF screenRect = new RectF(transPositionToScreenCoordinate(rectF.left, previewWidth),
                transPositionToScreenCoordinate(rectF.top, previewHeight),
                transPositionToScreenCoordinate(rectF.right, previewWidth),
                transPositionToScreenCoordinate(rectF.bottom, previewHeight));
        return screenRect;
    }

    /**
     * 将相机坐标转换为屏幕坐标
     *
     * @param position 相机坐标某个方向的坐标值(如x)
     * @param srcSize  相机坐标对应方向的尺寸(如width)
     * @return 屏幕坐标下该方向的坐标值
     */
    private static float transPositionToScreenCoordinate(float position, int srcSize) {
        return ((position + CAMERA_COORDINATE_HALF_SIZE) / CAMERA_COORDINATE_SIZE) * srcSize;
    }

    /**
     * RectF转为Rect
     *
     * @param rectF rectF对象
     * @return rect对象
     */
    private static Rect rectFToRect(RectF rectF) {
        if (rectF == null) {
            return new Rect(0, 0, 0, 0);
        }
        Rect rect = new Rect();
        rect.left = (int) rectF.left;
        rect.top = (int) rectF.top;
        rect.right = (int) rectF.right;
        rect.bottom = (int) rectF.bottom;
        return rect;
    }
}

矩形区域信息类CameraAreaEntity.java:

package com.example.study.entities;

public class CameraAreaEntity {
    /**
     * x轴坐标
     */
    private float x;

    /**
     * y轴坐标
     */
    private float y;

    /**
     * 矩形区域边长
     */
    private int areaSide;

    /**
     * 矩形区域边长长度系数
     */
    private float coefficient;

    /**
     * 预览画面的旋转角度
     */
    private int orientation;

    /**
     * 屏幕宽,也就是屏幕坐标轴的x轴
     */
    private int previewWidth;

    /**
     * 屏幕高,也就是屏幕坐标轴的y轴
     */
    private int previewHeight;

    public float getX() {
        return x;
    }

    public void setX(float x) {
        this.x = x;
    }

    public float getY() {
        return y;
    }

    public void setY(float y) {
        this.y = y;
    }

    public int getAreaSide() {
        return areaSide;
    }

    public void setAreaSide(int areaSide) {
        this.areaSide = areaSide;
    }

    public float getCoefficient() {
        return coefficient;
    }

    public void setCoefficient(float coefficient) {
        this.coefficient = coefficient;
    }

    public int getOrientation() {
        return orientation;
    }

    public void setOrientation(int orientation) {
        this.orientation = orientation;
    }

    public int getPreviewWidth() {
        return previewWidth;
    }

    public void setPreviewWidth(int previewWidth) {
        this.previewWidth = previewWidth;
    }

    public int getPreviewHeight() {
        return previewHeight;
    }

    public void setPreviewHeight(int previewHeight) {
        this.previewHeight = previewHeight;
    }
}

3. 应用于相机

布局文件activity_camera_demo.xml:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <SurfaceView
        android:id="@+id/camera_preview"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

    <com.example.study.views.DrawView
        android:id="@+id/camera_preview_draw"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</FrameLayout>

绘制对焦框的DrawView.java:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <SurfaceView
        android:id="@+id/camera_preview"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

    <com.example.study.views.DrawView
        android:id="@+id/camera_preview_draw"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</FrameLayout>

触摸监听类TouchListener.java:

package com.example.study.listeners;

import android.graphics.Color;
import android.graphics.Rect;
import android.hardware.Camera;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;

import com.example.study.entities.CameraAreaEntity;
import com.example.study.utils.CameraAreaUtils;
import com.example.study.views.DrawView;

import java.util.ArrayList;
import java.util.List;

public class TouchListener implements View.OnTouchListener {
    private static final String TAG = "TouchListener";
    private static final int AREA_SIDE = 210;
    private int orientation;
    private int leftMargin;
    private int topMargin;
    private Camera camera;
    private DrawView drawView;

    public TouchListener(Camera camera, int orientation, DrawView drawView, int leftMargin, int topMargin) {
        this.camera = camera;
        this.orientation = orientation;
        this.drawView = drawView;
        this.leftMargin = leftMargin;
        this.topMargin = topMargin;
    }

    @Override
    public boolean onTouch(View view, MotionEvent event) {
        try {
            handlerFocusAndMetering(event);
        } catch (Exception exception) {
            Log.i(TAG, exception.getMessage());
        }
        return true;
    }

    private void handlerFocusAndMetering(MotionEvent event) {
        // 只有一根手指且按下时,才设置对焦区域
        if (event.getPointerCount() != 1 || event.getActionMasked() != MotionEvent.ACTION_DOWN) {
            return;
        }
        drawView.clear();
        drawView.setColor(Color.WHITE);
        boolean supportSetArea = false;
        CameraAreaEntity areaEntity = createAreaEntity(event);
        drawView.addPoint(areaEntity.getX(), areaEntity.getY());
        Camera.Parameters parameters = camera.getParameters();
        // 检查是否支持设置对焦区域
        if (parameters.getMaxNumFocusAreas() > 0) {
            areaEntity.setCoefficient(1.0f);
            parameters.setFocusAreas(getAreas(areaEntity));
            // 绘制对焦区域
            drawView.addRect(CameraAreaUtils.createRect(areaEntity));
            supportSetArea = true;
        }
        // 检查是否支持设置测光区域
        if (parameters.getMaxNumMeteringAreas() > 0) {
            areaEntity.setCoefficient(1.5f);
            parameters.setMeteringAreas(getAreas(areaEntity));
            // 绘制测光区域
            drawView.addRect(CameraAreaUtils.createRect(areaEntity));
            supportSetArea = true;
        }
        drawView.setBackgroundColor(Color.TRANSPARENT);
        drawView.postInvalidate();
        if (!supportSetArea) {
            return;
        }
        String currentFocusMode = parameters.getFocusMode();
        parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_MACRO);
        camera.cancelAutoFocus();
        camera.setParameters(parameters);
        camera.autoFocus((success, camera) -> {
            drawRealArea(camera, areaEntity);
            Camera.Parameters params = camera.getParameters();
            params.setFocusMode(currentFocusMode);
            camera.setParameters(params);
        });
    }

    private CameraAreaEntity createAreaEntity(MotionEvent event) {
        // adjustSurface调整过左、上边距,所以需要加上边距
        float x = event.getX() + leftMargin;
        float y = event.getY() + topMargin;
        Camera.Parameters parameters = camera.getParameters();
        Camera.Size previewSize = parameters.getPreviewSize();
        CameraAreaEntity areaEntity = new CameraAreaEntity();
        areaEntity.setX(x);
        areaEntity.setY(y);
        areaEntity.setAreaSide(AREA_SIDE);
        areaEntity.setOrientation(orientation);
        areaEntity.setPreviewWidth(previewSize.height);
        areaEntity.setPreviewHeight(previewSize.width);
        return areaEntity;
    }

    private void drawRealArea(Camera camera, CameraAreaEntity areaEntity) {
        List<Camera.Area> focusAreas = camera.getParameters().getFocusAreas();
        if (focusAreas == null) {
            return;
        }
        drawView.clear();
        drawView.setColor(Color.BLUE);
        for (Camera.Area focusArea : focusAreas) {
            // previewSize的width和height正好相反
            Rect rect = CameraAreaUtils.transToScreen(focusArea.rect, areaEntity);
            drawView.addRect(rect);
        }
        drawView.setBackgroundColor(Color.TRANSPARENT);
        drawView.postInvalidate();
    }

    private List<Camera.Area> getAreas(CameraAreaEntity areaEntity) {
        List<Camera.Area> areas = new ArrayList<>();
        // weight:权重,取值范围为1-1000
        areas.add(new Camera.Area(CameraAreaUtils.transToCamera(areaEntity), 800));
        return areas;
    }
}

activity类:

package com.example.study.activities;

import android.Manifest;
import android.content.Context;
import android.content.pm.PackageManager;
import android.graphics.ImageFormat;
import android.graphics.Point;
import android.hardware.Camera;
import android.os.Bundle;
import android.util.Log;
import android.view.Display;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.WindowManager;
import android.widget.FrameLayout;

import androidx.activity.ComponentActivity;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import com.example.study.R;
import com.example.study.listeners.TouchListener;
import com.example.study.views.DrawView;

public class CameraDemoActivity extends ComponentActivity implements Camera.PreviewCallback, SurfaceHolder.Callback {
    private static final String TAG = "CameraDemoActivity";
    private static final int REQUEST_CAMERA = 1000;
    private static final int HEIGHT = 1920;
    private static final int WIDTH = 1080;
    private static final int ORIENTATION = 90;
    private int leftMargin = 0;
    private int topMargin = 0;
    private SurfaceView preview;
    private DrawView drawView;
    private Camera camera;
    private Camera.Parameters parameters;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        this.setContentView(R.layout.activity_camera_demo);
        preview = findViewById(R.id.camera_preview);
        drawView = findViewById(R.id.camera_preview_draw);
        adjustSurface(preview);

        // 检查权限
        if (checkSelfPermission(Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
            requestPermissions(new String[]{Manifest.permission.CAMERA}, REQUEST_CAMERA);
        } else {
            preview.getHolder().addCallback(this);
        }
    }

    private void adjustSurface(SurfaceView cameraPreview) {
        FrameLayout.LayoutParams paramSurface = (FrameLayout.LayoutParams) cameraPreview.getLayoutParams();
        if (getSystemService(Context.WINDOW_SERVICE) != null) {
            WindowManager windowManager = (WindowManager) getSystemService(Context.WINDOW_SERVICE);
            Display defaultDisplay = windowManager.getDefaultDisplay();
            Point outPoint = new Point();
            defaultDisplay.getRealSize(outPoint);
            float screenWidth = outPoint.x;
            float screenHeight = outPoint.y;
            float rate;
            if (screenWidth / (float) WIDTH > screenHeight / (float) HEIGHT) {
                rate = screenWidth / (float) WIDTH;
                int targetHeight = (int) (HEIGHT * rate);
                paramSurface.width = FrameLayout.LayoutParams.MATCH_PARENT;
                paramSurface.height = targetHeight;
                topMargin = (int) (-(targetHeight - screenHeight) / 2);
                if (topMargin < 0) {
                    paramSurface.topMargin = topMargin;
                }
            } else {
                rate = screenHeight / (float) HEIGHT;
                int targetWidth = (int) (WIDTH * rate);
                paramSurface.width = targetWidth;
                paramSurface.height = FrameLayout.LayoutParams.MATCH_PARENT;
                leftMargin = (int) (-(targetWidth - screenWidth) / 2);
                if (leftMargin < 0) {
                    paramSurface.leftMargin = leftMargin;
                }
            }
        }
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        if (requestCode == REQUEST_CAMERA && grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
            preview.getHolder().addCallback(this);
            surfaceCreated(preview.getHolder());
            camera.setPreviewCallback(this);
            camera.startPreview();
        }
    }

    @Override
    public void onPreviewFrame(byte[] data, Camera camera) {
        Log.i(TAG, "接收到一帧图片");
    }

    @Override
    public void surfaceCreated(@NonNull SurfaceHolder holder) {
        try {
            camera = Camera.open(Camera.CameraInfo.CAMERA_FACING_BACK);
            parameters = camera.getParameters();
            // 旋转了90度,所以height、width互换
            parameters.setPictureSize(HEIGHT, WIDTH);
            parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE);
            parameters.setPictureFormat(ImageFormat.NV21);
            camera.setPreviewDisplay(holder);
            camera.setDisplayOrientation(ORIENTATION);
            camera.setParameters(parameters);
            preview.setOnTouchListener(new TouchListener(camera, ORIENTATION, drawView, leftMargin, topMargin));
        } catch (Exception exception) {
            Log.i(TAG, exception.getMessage());
        }
    }

    @Override
    public void surfaceChanged(@NonNull SurfaceHolder holder, int format, int width, int height) {
        if (camera != null) {
            camera.stopPreview();
            camera.setPreviewCallback(null);
            camera.startPreview();
            camera.setPreviewCallback(this);
        }
    }

    @Override
    public void surfaceDestroyed(@NonNull SurfaceHolder holder) {
        if (camera != null) {
            camera.stopPreview();
            camera.setPreviewCallback(null);
            camera.release();
        }
    }
}

http://www.kler.cn/a/470071.html

相关文章:

  • 【python】matplotlib(radar chart)
  • C++二十三种设计模式之抽象工厂模式
  • 使用 Python结合ffmpeg 实现单线程和多线程推流
  • Leetcode 3414. Maximum Score of Non-overlapping Intervals
  • STM32的LED点亮教程:使用HAL库与Proteus仿真
  • 华为 Sensor 省电策略调研
  • Python应用——将Matplotlib图形嵌入Tkinter窗口
  • 分布式环境下定时任务扫描时间段模板创建可预订时间段
  • Unity学习笔记(七)使用状态机重构角色攻击
  • Qt菜单栏、工具栏、状态栏(右键)
  • Linux(Ubuntu24.04)安装Eigen3库
  • 公共数据授权运营机制建设(六大机制、存在问题、发展路径)
  • mongodb==安装prisma连接
  • (leetcode算法题)面试题 17.19. 消失的两个数字
  • 01、Redis初认识
  • 哈希表笔记
  • 卡诺图化简最小项表达式
  • leetcode 2274. 不含特殊楼层的最大连续楼层数 中等
  • 后台运行 Python
  • JVM实战—MAT的使用和JVM优化总结
  • pip error: microsoft visual c++ 14.0 or greater is required
  • 文档解析工具:如何将PDF文档转换为Markdown格式?
  • 【FlutterDart】 listView.builder例子二(14 /100)
  • 练习(继承)
  • 25考研|重邮软件工程复试攻略!
  • 数据结构-单链表及其应用