安卓触摸对焦

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.CameraAreaInfo;

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 areaInfo 矩形区域信息
     * @return 矩形
     */
    public static Rect createDisplayRect(CameraAreaInfo areaInfo) {
        float x = areaInfo.getX() + areaInfo.getLeftMargin();
        float y = areaInfo.getY() + areaInfo.getTopMargin();
        int displayWidth = areaInfo.getDisplayWidth();
        int displayHeight = areaInfo.getDisplayHeight();
        int sideSize = (int) (areaInfo.getAreaSide() * areaInfo.getCoefficient());
        int left = clamp(x - sideSize / 2, 0, displayWidth);
        int top = clamp(y - sideSize / 2, 0, displayHeight);
        int right = clamp(x + sideSize / 2, 0, displayWidth);
        int bottom = clamp(y + sideSize / 2, 0, displayHeight);
        if (right - left < sideSize) {
            if (left == 0) {
                right = Math.min(sideSize, displayWidth);
            }
            if (right == displayWidth) {
                left = Math.max(displayWidth - sideSize, 0);
            }
        }
        if (bottom - top < sideSize) {
            if (top == 0) {
                bottom = Math.min(sideSize, displayHeight);
            }
            if (bottom == displayHeight) {
                top = Math.max(displayHeight - sideSize, 0);
            }
        }
        return new Rect(left, top, right, bottom);
    }

    /**
     * 屏幕显示的rect转为显示预览画面的SurfaceView的rect
     *
     * @param areaInfo 矩形区域信息
     * @param rect     屏幕显示的rect
     */
    public static void displayToView(CameraAreaInfo areaInfo, Rect rect) {
        rect.left = clamp(rect.left - areaInfo.getLeftMargin(), 0, areaInfo.getViewWidth());
        rect.top = clamp(rect.top - areaInfo.getTopMargin(), 0, areaInfo.getViewHeight());
        rect.right = clamp(rect.right - areaInfo.getLeftMargin(), 0, areaInfo.getViewWidth());
        rect.bottom = clamp(rect.bottom - areaInfo.getTopMargin(), 0, areaInfo.getViewHeight());
    }

    /**
     * 屏幕矩形区域转为相机矩形区域
     *
     * @param areaInfo 矩形区域信息
     * @return 相机矩形区域
     */
    public static Rect transToCamera(CameraAreaInfo areaInfo) {
        Rect rect = createDisplayRect(areaInfo);
        displayToView(areaInfo, rect);
        Rect cameraRect = rectFToRect(transToCamera(new RectF(rect), areaInfo));
        cameraRect.left = clamp(cameraRect.left, -CAMERA_COORDINATE_HALF_SIZE, CAMERA_COORDINATE_HALF_SIZE);
        cameraRect.top = clamp(cameraRect.top, -CAMERA_COORDINATE_HALF_SIZE, CAMERA_COORDINATE_HALF_SIZE);
        cameraRect.right = clamp(cameraRect.right, -CAMERA_COORDINATE_HALF_SIZE, CAMERA_COORDINATE_HALF_SIZE);
        cameraRect.bottom = clamp(cameraRect.bottom, -CAMERA_COORDINATE_HALF_SIZE, CAMERA_COORDINATE_HALF_SIZE);
        return cameraRect;
    }

    /**
     * 屏幕矩形区域转为相机矩形区域
     *
     * @param rectF    屏幕矩形区域
     * @param areaInfo 矩形区域信息
     * @return 相机矩形区域
     */
    private static RectF transToCamera(RectF rectF, CameraAreaInfo areaInfo) {
        int viewWidth = areaInfo.getViewWidth();
        int viewHeight = areaInfo.getViewHeight();
        RectF cameraRectF = new RectF(transPositionToCameraCoordinate(rectF.left, viewWidth),
                transPositionToCameraCoordinate(rectF.top, viewHeight),
                transPositionToCameraCoordinate(rectF.right, viewWidth),
                transPositionToCameraCoordinate(rectF.bottom, viewHeight));
        // 预览画面如果有旋转,映射到相机坐标后,需要把旋转取消,才能对应真正的相机坐标
        Matrix matrix = new Matrix();
        matrix.setRotate(-1 * areaInfo.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 areaInfo 矩形区域信息
     * @return 屏幕矩形区域
     */
    public static Rect transToDisplay(Rect rect, CameraAreaInfo areaInfo) {
        return transToDisplay(new RectF(rect), areaInfo);
    }

    /**
     * 相机矩形区域转为屏幕矩形区域
     *
     * @param rectF    相机矩形区域
     * @param areaInfo 矩形区域信息
     * @return 屏幕矩形区域
     */
    private static Rect transToDisplay(RectF rectF, CameraAreaInfo areaInfo) {
        // 先把相机坐标按预览的旋转角度进行旋转
        Matrix matrix = new Matrix();
        matrix.setRotate(areaInfo.getOrientation());
        matrix.mapRect(rectF);
        int viewWidth = areaInfo.getViewWidth();
        int viewHeight = areaInfo.getViewHeight();
        // 先转为显示预览画面的SurfaceView的rect
        RectF viewRect = new RectF(transPositionToScreenCoordinate(rectF.left, viewWidth),
                transPositionToScreenCoordinate(rectF.top, viewHeight),
                transPositionToScreenCoordinate(rectF.right, viewWidth),
                transPositionToScreenCoordinate(rectF.bottom, viewHeight));
        int displayWidth = areaInfo.getDisplayWidth();
        int displayHeight = areaInfo.getDisplayHeight();
        // 再转为屏幕显示的rect
        Rect rect = new Rect();
        rect.left = clamp(viewRect.left + areaInfo.getLeftMargin(), 0, displayWidth);
        rect.top = clamp(viewRect.top + areaInfo.getTopMargin(), 0, displayHeight);
        rect.right = clamp(viewRect.right + areaInfo.getLeftMargin(), 0, displayWidth);
        rect.bottom = clamp(viewRect.bottom + areaInfo.getTopMargin(), 0, displayHeight);
        return rect;
    }

    /**
     * 将相机坐标转换为屏幕坐标
     *
     * @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;
    }

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

    /**
     * 防止坐标越界
     *
     * @param val 坐标
     * @param min 最小值
     * @param max 最大值
     * @return 坐标
     */
    private static int clamp(int val, int min, int max) {
        return val < min ? min : val > max ? max : val;
    }

    /**
     * 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;
    }
}

矩形区域信息类CameraAreaInfo.java:

package com.example.study.entities;

public class CameraAreaInfo {
    /**
     * 相对于显示预览画面的SurfaceView的x轴坐标
     */
    private float x;

    /**
     * 相对于显示预览画面的SurfaceView的y轴坐标
     */
    private float y;

    /**
     * 矩形区域边长(以显示预览画面的View为标准)
     */
    private int areaSide;

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

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

    /**
     * 显示预览画面的SurfaceView的左边距
     */
    private int leftMargin;

    /**
     * 显示预览画面的SurfaceView的上边距
     */
    private int topMargin;

    /**
     * 在屏幕显示出来的宽
     */
    private int displayWidth;

    /**
     * 在屏幕显示出来的高
     */
    private int displayHeight;

    /**
     * 显示预览画面的SurfaceView的宽
     */
    private int viewWidth;

    /**
     * 显示预览画面的SurfaceView的高
     */
    private int viewHeight;

    /**
     * 图片实际的宽(parameters.setPictureSize)
     */
    private int realWidth;

    /**
     * 图片实际的高(parameters.setPictureSize)
     */
    private int realHeight;

    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 getLeftMargin() {
        return leftMargin;
    }

    public void setLeftMargin(int leftMargin) {
        this.leftMargin = leftMargin;
    }

    public int getTopMargin() {
        return topMargin;
    }

    public void setTopMargin(int topMargin) {
        this.topMargin = topMargin;
    }

    public int getDisplayWidth() {
        return displayWidth;
    }

    public void setDisplayWidth(int displayWidth) {
        this.displayWidth = displayWidth;
    }

    public int getDisplayHeight() {
        return displayHeight;
    }

    public void setDisplayHeight(int displayHeight) {
        this.displayHeight = displayHeight;
    }

    public int getViewWidth() {
        return viewWidth;
    }

    public void setViewWidth(int viewWidth) {
        this.viewWidth = viewWidth;
    }

    public int getViewHeight() {
        return viewHeight;
    }

    public void setViewHeight(int viewHeight) {
        this.viewHeight = viewHeight;
    }

    public int getRealWidth() {
        return realWidth;
    }

    public void setRealWidth(int realWidth) {
        this.realWidth = realWidth;
    }

    public int getRealHeight() {
        return realHeight;
    }

    public void setRealHeight(int realHeight) {
        this.realHeight = realHeight;
    }
}

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"
    android:background="#C0C0C0">

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

    <!--屏幕显示区域的画面-->
    <ImageView
        android:id="@+id/camera_preview_view"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginLeft="200dp"
        android:layout_marginTop="500dp" />

    <!--对焦区域的画面-->
    <ImageView
        android:id="@+id/camera_rect_view"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginLeft="50dp"
        android:layout_marginTop="500dp" />

    <!--绘制对焦框和测光框-->
    <com.example.study.views.DrawView
        android:id="@+id/camera_preview_draw"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</FrameLayout>

绘制对焦框的DrawView.java:

package com.example.study.views;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.view.View;

import androidx.annotation.Nullable;

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

public class DrawView extends View {

    private final Object lock = new Object();

    protected Paint paint;

    protected int color;

    private final List<Rect> rects = new ArrayList<>();

    private final List<float[]> dots = new ArrayList<>();

    public DrawView(Context context) {
        super(context);
    }

    public DrawView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    public void clear() {
        synchronized (lock) {
            rects.clear();
            dots.clear();
        }
        postInvalidate();
    }

    public void addRect(Rect rect) {
        synchronized (lock) {
            rects.add(rect);
        }
    }

    public void addPoint(float x, float y) {
        synchronized (lock) {
            dots.add(new float[]{x, y});
        }
    }

    public void setColor(int color) {
        this.color = color;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        synchronized (lock) {
            paint = new Paint();

            paint.setStyle(Paint.Style.STROKE);
            paint.setColor(color);
            for (Rect rect : rects) {
                paint.setStrokeWidth(4);
                canvas.drawRect(rect, paint);
            }
            for (float[] dot : dots) {
                paint.setStrokeWidth(10);
                canvas.drawPoint(dot[0], dot[1], paint);
            }
        }
    }
}

触摸监听类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.CameraAreaInfo;
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 CameraAreaInfo areaInfo;
    private Camera camera;
    private DrawView drawView;
    private TouchCallback callback;

    public TouchListener(Camera camera, CameraAreaInfo areaInfo, DrawView drawView, TouchCallback callback) {
        this.camera = camera;
        this.areaInfo = areaInfo;
        this.drawView = drawView;
        if (checkAreaInfo(areaInfo)) {
            this.areaInfo = areaInfo;
        } else {
            throw new RuntimeException("areaInfo can not be null!");
        }
        this.callback = callback;
    }

    private boolean checkAreaInfo(CameraAreaInfo areaInfo) {
        return areaInfo.getDisplayWidth() > 0 && areaInfo.getDisplayHeight() > 0
                && areaInfo.getViewWidth() > 0 && areaInfo.getViewHeight() > 0
                && areaInfo.getRealWidth() > 0 && areaInfo.getRealHeight() > 0;
    }

    @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;

        areaInfo.setX(event.getX());
        areaInfo.setY(event.getY());
        areaInfo.setAreaSide(AREA_SIDE);
        drawView.addPoint(event.getX() + areaInfo.getLeftMargin(), event.getY() + areaInfo.getTopMargin());
        Camera.Parameters parameters = camera.getParameters();
        // 检查是否支持设置对焦区域
        if (parameters.getMaxNumFocusAreas() > 0) {
            areaInfo.setCoefficient(1.0f);
            parameters.setFocusAreas(getAreas(areaInfo));
            // 绘制对焦区域
            drawView.addRect(CameraAreaUtils.createDisplayRect(areaInfo));
            supportSetArea = true;
        }
        // 检查是否支持设置测光区域
        if (parameters.getMaxNumMeteringAreas() > 0) {
            areaInfo.setCoefficient(1.5f);
            parameters.setMeteringAreas(getAreas(areaInfo));
            // 绘制测光区域
            drawView.addRect(CameraAreaUtils.createDisplayRect(areaInfo));
            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, areaInfo);
            Camera.Parameters params = camera.getParameters();
            params.setFocusMode(currentFocusMode);
            camera.setParameters(params);
        });
        callback.onTouch(areaInfo);
    }

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

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

    public interface TouchCallback {
        void onTouch(CameraAreaInfo info);
    }
}

activity类:

package com.example.study.activities;

import android.Manifest;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Context;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.ImageFormat;
import android.graphics.Matrix;
import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.YuvImage;
import android.hardware.Camera;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;
import android.view.Display;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.FrameLayout;
import android.widget.ImageView;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import com.example.study.R;
import com.example.study.entities.CameraAreaInfo;
import com.example.study.listeners.TouchListener;
import com.example.study.utils.CameraAreaUtils;
import com.example.study.views.DrawView;

import java.io.ByteArrayOutputStream;

public class CameraDemoActivity extends Activity 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 static final Matrix MATRIX = new Matrix();
    private CameraAreaInfo areaInfo;
    private SurfaceView preview;
    private DrawView drawView;
    private ImageView previewImageView;
    private ImageView rectImageView;
    private Camera camera;
    private Camera.Parameters parameters;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        if (Build.VERSION.SDK_INT < 16) {
            getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);
        } else {
            getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN);
        }
        this.setContentView(R.layout.activity_camera_demo);

        preview = findViewById(R.id.camera_preview);
        drawView = findViewById(R.id.camera_preview_draw);
        previewImageView = findViewById(R.id.camera_preview_view);
        rectImageView = findViewById(R.id.camera_rect_view);
        adjustSurface(preview);
        adjustOthers();

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

    private void adjustOthers() {
        ViewGroup.LayoutParams layoutParams = previewImageView.getLayoutParams();
        layoutParams.width = areaInfo.getDisplayWidth() / 5;
        layoutParams.height = areaInfo.getDisplayHeight() / 5;
    }

    private void adjustSurface(SurfaceView cameraPreview) {
        WindowManager windowManager = (WindowManager) getSystemService(Context.WINDOW_SERVICE);
        if (windowManager == null) {
            return;
        }
        FrameLayout.LayoutParams paramSurface = (FrameLayout.LayoutParams) cameraPreview.getLayoutParams();
        Display defaultDisplay = windowManager.getDefaultDisplay();
        Point outPoint = new Point();
        defaultDisplay.getSize(outPoint);
        int displayWidth = outPoint.x;
        int displayHeight = outPoint.y;
        int viewWidth = displayWidth;
        int viewHeight = displayHeight;
        int leftMargin = 0;
        int topMargin = 0;
        // 标准宽高比:displayWidth/displayHeight == WIDTH/HEIGHT;
        if (displayWidth * HEIGHT < WIDTH * displayHeight) {
            // 高更长:displayWidth/displayHeight < WIDTH/HEIGHT -> displayWidth * HEIGHT < WIDTH * displayHeight
            viewWidth = (int) ((float) displayHeight / HEIGHT * WIDTH);
            leftMargin = -(viewWidth - displayWidth) / 2;
        } else if (displayWidth * HEIGHT > WIDTH * displayHeight) {
            // 宽更长:displayWidth/displayHeight > WIDTH/HEIGHT -> displayWidth * HEIGHT > WIDTH * displayHeight
            viewHeight = (int) ((float) displayWidth / WIDTH * HEIGHT);
            topMargin = -(viewHeight - displayHeight) / 2;
        }
        paramSurface.width = viewWidth;
        paramSurface.height = viewHeight;
        paramSurface.leftMargin = leftMargin;
        paramSurface.topMargin = topMargin;
        areaInfo = new CameraAreaInfo();
        areaInfo.setOrientation(ORIENTATION);
        areaInfo.setLeftMargin(leftMargin);
        areaInfo.setTopMargin(topMargin);
        areaInfo.setDisplayWidth(displayWidth);
        areaInfo.setDisplayHeight(displayHeight);
        areaInfo.setViewWidth(viewWidth);
        areaInfo.setViewHeight(viewHeight);
        areaInfo.setRealWidth(WIDTH);
        areaInfo.setRealHeight(HEIGHT);
    }

    @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, "接收到一帧图片");
        Bitmap bitmap = convertToBitmap(camera, data);
        // 预览画面经过旋转,这里先旋转
        Bitmap rotateBitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), MATRIX, false);
        // viewWidth/viewHeight跟屏幕显示的大小不一定相同,根据显示区域大小跟view的大小比值,计算裁剪尺寸
        Bitmap displayBitmap = Bitmap.createBitmap(rotateBitmap,
                Math.max((int) ((float) (-areaInfo.getLeftMargin()) / areaInfo.getViewWidth() * areaInfo.getRealWidth()), 0),
                Math.max((int) ((float) (-areaInfo.getTopMargin()) / areaInfo.getViewHeight() * areaInfo.getRealHeight()), 0),
                Math.min((int) ((float) areaInfo.getDisplayWidth() / areaInfo.getViewWidth() * areaInfo.getRealWidth()), areaInfo.getRealWidth()),
                Math.min((int) ((float) areaInfo.getDisplayHeight() / areaInfo.getViewHeight() * areaInfo.getRealHeight()), areaInfo.getRealHeight()));
        previewImageView.setImageBitmap(displayBitmap);

        areaInfo.setCoefficient(1.0f);
        if (areaInfo.getX() > 0 && areaInfo.getY() > 0) {
            Rect displayRect = CameraAreaUtils.createDisplayRect(areaInfo);
            // 根据显示区域大小跟view的大小比值,计算裁剪尺寸
            Bitmap displayRectBitmap = Bitmap.createBitmap(rotateBitmap,
                    Math.max((int) ((float) (displayRect.left - areaInfo.getLeftMargin()) / areaInfo.getViewWidth() * areaInfo.getRealWidth()), 0),
                    Math.max((int) ((float) (displayRect.top - areaInfo.getTopMargin()) / areaInfo.getViewHeight() * areaInfo.getRealHeight()), 0),
                    Math.min((int) ((float) (displayRect.right - displayRect.left) / areaInfo.getViewWidth() * areaInfo.getRealWidth()), areaInfo.getRealWidth()),
                    Math.min((int) ((float) (displayRect.bottom - displayRect.top) / areaInfo.getViewHeight() * areaInfo.getRealHeight()), areaInfo.getRealHeight()));
            rectImageView.setImageBitmap(displayRectBitmap);
            ViewGroup.LayoutParams layoutParams = rectImageView.getLayoutParams();
            layoutParams.height = (int) ((float) displayRectBitmap.getHeight() / areaInfo.getRealHeight() * areaInfo.getViewHeight());
            layoutParams.width = (int) ((float) displayRectBitmap.getWidth() / areaInfo.getRealWidth() * areaInfo.getViewWidth());
        }
    }

    /**
     * Convert camera data into bitmap data.
     */
    private Bitmap convertToBitmap(Camera camera, byte[] data) {
        int width = camera.getParameters().getPreviewSize().width;
        int height = camera.getParameters().getPreviewSize().height;
        YuvImage yuv = new YuvImage(data, ImageFormat.NV21, width, height, null);
        ByteArrayOutputStream stream = new ByteArrayOutputStream();
        yuv.compressToJpeg(new Rect(0, 0, width, height), 100, stream);
        return BitmapFactory.decodeByteArray(stream.toByteArray(), 0, stream.toByteArray().length);
    }

    @SuppressLint("ClickableViewAccessibility")
    @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, areaInfo, drawView, (areaInfo) -> {
                this.areaInfo.setX(areaInfo.getX());
                this.areaInfo.setY(areaInfo.getY());
                this.areaInfo.setAreaSide(areaInfo.getAreaSide());
                this.areaInfo.setCoefficient(1.0f);
            }));
            MATRIX.setRotate(ORIENTATION);
        } 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();
        }
    }
}
<think>嗯,用户想了解Excel中的VLOOKUP函数的用法和参数含义。首先,我需要回忆一下VLOOKUP的基本结构。VLOOKUP是垂直查找函数,用于在表格的首列查找指定的值,然后返回该行中指定列的数据。它的四个参数分别是查找值、表格范围、列索引号和匹配模式。 接下来,我得确认每个参数的具体作用。第一个参数是lookup_value,也就是用户要查找的值,比如产品ID或者姓名。第二个参数是table_array,即查找的范围,这里要注意的是,查找值必须位于这个范围的第一列,否则函数无法正确工作。第三个参数是col_index_num,也就是用户想要返回的数据在表格范围中的第几列,这个列数是从表格范围的第一列开始算起的,不是整个工作表的列。第四个参数是range_lookup,决定是精确匹配还是近似匹配,通常用FALSE或0来指定精确匹配,避免错误。 然后,用户可能需要一个具体的例子来理解如何使用。比如,假设有一个产品价格表,用户想根据产品ID查找对应的价格。这时候,VLOOKUP就可以在ID列查找,然后返回价格列的数据。同时,要提醒用户注意表格范围最好使用绝对引用,比如$A$2:$C$100,这样在拖动公式时范围不会变化。 可能遇到的问题也需要考虑。比如,如果查找值不在表格的第一列,或者列索引号超过了表格的列数,函数会返回错误。此外,数据格式不一致也可能导致问题,比如查找值是数字,而表格中是文本格式的数字,这时候需要统一格式。还有,当出现#N/A错误时,可能是查找值不存在,或者匹配模式设置不正确,需要检查参数是否正确。 用户可能还会问到如何避免常见的错误,比如部分数据匹配出错的情况。这时候需要检查是否有多余的空格,或者数据格式不一致,或者表格范围没有锁定导致拖动时偏移。此外,XLOOKUP作为VLOOKUP的升级版,可能在某些情况下更方便,比如可以向左查找或者返回多列数据,这也是可以提及的内容。 最后,整理步骤,从基础语法到参数说明,再到具体案例,逐步引导用户理解如何使用VLOOKUP函数。同时,提供常见错误和解决方法,帮助用户在实际应用中避免问题。如果有必要,还可以比较VLOOKUP和其他查找函数的区别,比如HLOOKUP或XLOOKUP,但根据用户的问题,主要集中在VLOOKUP本身即可。</think>### Excel VLOOKUP函数详解 #### 一、基础语法与参数说明 VLOOKUP函数的语法为: $$=VLOOKUP(lookup\_value, table\_array, col\_index\_num, [range\_lookup])$$ 包含4个参数: 1. **lookup_value**(必填):要查找的值(如单元格引用或具体值) 2. **table_array**(必填):包含数据的表格范围(必须包含查找列和返回列) 3. **col_index_num**(必填):返回值所在列的序号(从table_array第一列开始计数) 4. **range_lookup**(可选):匹配类型 - `TRUE`/`1`:近似匹配(默认值,需数据升序排列) - `FALSE`/`0`:精确匹配(常用选项) [^1][^2] #### 二、使用步骤演示(工资表查询案例) 假设需要根据员工编号查询工资: 1. 建立查询单元格(如`B12`) 2. 输入公式: ```excel =VLOOKUP(A12, $A$2:$D$100, 4, 0) ``` - `A12`:待查询的员工编号 - `$A$2:$D$100`:锁定数据区域(绝对引用) - `4`:返回第4列(工资列) - `0`:精确匹配 [^2][^3] #### 三、常见错误与解决方法 | 错误现象 | 原因 | 解决方案 | |---------|------|---------| | #N/A | 查找值不存在 | 检查数据源或改用`IFERROR`容错 | | #REF! | 列序号超出范围 | 确认col_index_num ≤ 表格列数 | | 部分匹配失败 | 数据格式不一致 | 统一数值/文本格式 | | 结果错位 | 表格未锁定 | 使用`$`符号固定区域引用 | [^3][^4] #### 四、进阶技巧 1. **多条件查询**: 使用辅助列合并多个条件字段 ```excel =VLOOKUP(A2&B2, $D$2:$F$100, 3, 0) ``` 2. **通配符匹配**: `"*"`匹配任意字符,`"?"`匹配单个字符 ```excel =VLOOKUP("张*", $A$2:$C$100, 3, 0) ``` 3. **跨表查询**: 引用其他工作表数据 ```excel =VLOOKUP(A2, Sheet2!$A$2:$D$100, 4, 0) ``` [^1][^4]
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值