Android自定义相机的适配

自定义相机

Posted by River on December 20, 2019

前言

最近支援bos端新app,接到一个自定义相机的需求; 因为需要适配大部分机型预览不变形,历时良久,特此记录。

1、核心实现类

核心实现类CameraPreview

    public class CameraPreview extends SurfaceView implements SurfaceHolder.Callback {

    static final String TAG = "CameraPreview";

    private Context context;
    /**
     * 预览尺寸集合
     */
    private final SizeMap mPreviewSizes = new SizeMap();
    /**
     * 图片尺寸集合
     */
    private final SizeMap mPictureSizes = new SizeMap();
    /**
     * 屏幕旋转显示角度
     */
    private int mDisplayOrientation;
    /**
     * 设备屏宽比
     */
    private AspectRatio mAspectRatio;

    private Camera mCamera;

    private boolean fitPreviewAndPicture = false;

    public CameraPreview(Context context) {
        super(context);
        initPreview(context);
    }

    public CameraPreview(Context context, AttributeSet attrs) {
        super(context, attrs);
        initPreview(context);
    }

    public CameraPreview(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initPreview(context);
    }

    private void initPreview(Context context) {
        this.context = context;
        getHolder().addCallback(this);
        mDisplayOrientation = ((Activity) context).getWindowManager().getDefaultDisplay().getRotation();
        mAspectRatio = AspectRatio.of(16, 9);
    }

    public Camera getCamera() {
        return mCamera;
    }

    @Override
    public void surfaceCreated(SurfaceHolder holder) {
        HiLogger.i(TAG, ",surfaceCreated...");
        try {
            initCamera();
            Activity activity = (Activity) context;
            //设置设备高宽比
            mAspectRatio = getDeviceAspectRatio(activity);
            //设置预览方向
            Camera.Parameters parameters = mCamera.getParameters();
            mCamera.setDisplayOrientation(90);

            //获取所有支持的预览尺寸
            mPreviewSizes.clear();
            //获取所有支持的图片尺寸
            mPictureSizes.clear();

            Size previewSize = new Size(1280, 720);
            Size pictureSize = new Size(1280, 720);

            for (Camera.Size size : parameters.getSupportedPreviewSizes()) {
                Log.i(TAG, "mPreviewSizes,size.width = "
                        + size.width + ":size.height = " + size.height);
                mPreviewSizes.add(new Size(size.width, size.height));
                if (activity.getWindow().getDecorView().getWidth() <= size.height) {
                    if (previewSize.getHeight() < size.height) {
                        previewSize = new Size(size.width, size.height);
                    }
                }
            }

            for (Camera.Size size : parameters.getSupportedPictureSizes()) {
                Log.i(TAG, "mPictureSizes,size.width = "
                        + size.width + ":size.height = " + size.height);
                mPictureSizes.add(new Size(size.width, size.height));

                if (fitPreviewAndPicture){
                    break;
                }

                if (activity.getWindow().getDecorView().getWidth() <= size.height) {
                    if (pictureSize.getHeight() < size.height) {
                        pictureSize = new Size(size.width, size.height);
                    }
                }else if (previewSize.getWidth() * pictureSize.getHeight()
                        != previewSize.getHeight() * pictureSize.getWidth()){

                    Log.i(TAG, "mPictureSizes,!= :pictureSize.getWidth() = "
                            + pictureSize.getWidth() +
                            ":pictureSize.getHeight() = " + pictureSize.getHeight());

                    if (previewSize.getWidth() * size.height ==
                            previewSize.getHeight() * size.width){

                        fitPreviewAndPicture = true;
                        Log.i(TAG, "better,mPictureSizes,== :size.width = "
                                + size.width+
                                ":size.height = " + size.height);

                        pictureSize = new Size(size.width, size.height);
                    }
                }
            }

            Size betterPreviewSize = chooseOptimalSize(mPreviewSizes.sizes(mAspectRatio));
            if (betterPreviewSize != null) {
//                previewSize = betterPreviewSize;
            }


            SortedSet<Size> sortedSet = mPictureSizes.sizes(mAspectRatio);

            if (sortedSet != null && sortedSet.size() > 0) {
//                pictureSize = sortedSet.last();
            }

            Log.d(TAG, "confirm,previewSize.getWidth() = " + previewSize.getWidth()
                    + ",previewSize.getHeight() = " + previewSize.getHeight());

            Log.d(TAG, "confirm,pictureSize.getWidth() = " + pictureSize.getWidth()
                    + ",pictureSize.getHeight() = " + pictureSize.getHeight());

            //设置相机参数
            parameters.setPreviewSize(previewSize.getWidth(), previewSize.getHeight());
            parameters.setPictureSize(pictureSize.getWidth(), pictureSize.getHeight());
            parameters.setPictureFormat(ImageFormat.JPEG);
            parameters.setRotation(90);
            mCamera.setParameters(parameters);
            //把这个预览效果展示在SurfaceView上面
            mCamera.setPreviewDisplay(holder);
            //开启预览效果
            mCamera.startPreview();
        } catch (IOException e) {
            HiLogger.e("CameraPreview", "相机预览错误: " + e.getMessage());
        }
    }

    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
        HiLogger.i(TAG, ",surfaceChanged...000");
    }

    @Override
    public boolean onCapturedPointerEvent(MotionEvent event) {
        HiLogger.i(TAG, "onCapturedPointerEvent");
        return super.onCapturedPointerEvent(event);
    }

    @SuppressLint("ClickableViewAccessibility")
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        doFocus(event);
        return super.onTouchEvent(event);
    }

    private void doFocus(MotionEvent event) {
        if (mOnFocusListener != null) {
            mOnFocusListener.onFocus(mCamera, event);
        }
    }


    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
        HiLogger.i(TAG, ",surfaceDestroyed...");
        releaseCamera();
    }

    private void initCamera() {
        if (mCamera == null) {
            mCamera = Camera.open();
        }
    }

    private void releaseCamera() {
        if (mCamera != null) {
            mCamera.release();
            mCamera = null;
        }
    }


    private AspectRatio getDeviceAspectRatio(Activity activity) {
        int width = activity.getWindow().getDecorView().getWidth();
        int height = activity.getWindow().getDecorView().getHeight();

        Log.i(TAG, "getDeviceAspectRatio,width = "
                + width + ":height = " + height);

        return AspectRatio.of(height, width);
    }


    @SuppressWarnings("SuspiciousNameCombination")
    private Size chooseOptimalSize(SortedSet<Size> sizes) {
        if (sizes == null || sizes.size() == 0) return null;
        int desiredWidth;
        int desiredHeight;
        final int surfaceWidth = getWidth();
        final int surfaceHeight = getHeight();
        if (isLandscape(mDisplayOrientation)) {
            desiredWidth = surfaceHeight;
            desiredHeight = surfaceWidth;
        } else {
            desiredWidth = surfaceWidth;
            desiredHeight = surfaceHeight;
        }
        Size result = null;
        for (Size size : sizes) {
            if (desiredWidth <= size.getWidth() && desiredHeight <= size.getHeight()) {
                return size;
            }
            result = size;
        }
        return result;
    }

    /**
     * @param orientationDegrees Orientation in degrees (0,90,180,270)
     * @return True if in landscape, false if portrait
     */
    private boolean isLandscape(int orientationDegrees) {
        return (orientationDegrees == 90 ||
                orientationDegrees == 270);
    }

    private OnFocusListener mOnFocusListener;

    public void setOnFocusListener(OnFocusListener listener) {
        mOnFocusListener = listener;
    }
}
    

2、辅助类

记录坐标AspectRatio

    
    public class AspectRatio implements Comparable<AspectRatio>, Parcelable {
    private final static SparseArrayCompat<SparseArrayCompat<AspectRatio>> sCache
            = new SparseArrayCompat<>(16);

    private final int mX;
    private final int mY;

    /**
     * @param x The width
     * @param y The height
     * @return An instance of {@link AspectRatio}
     */
    public static AspectRatio of(int x, int y) {
        int gcd = gcd(x, y);
        x /= gcd;
        y /= gcd;
        SparseArrayCompat<AspectRatio> arrayX = sCache.get(x);
        if (arrayX == null) {
            AspectRatio ratio = new AspectRatio(x, y);
            arrayX = new SparseArrayCompat<>();
            arrayX.put(y, ratio);
            sCache.put(x, arrayX);
            return ratio;
        } else {
            AspectRatio ratio = arrayX.get(y);
            if (ratio == null) {
                ratio = new AspectRatio(x, y);
                arrayX.put(y, ratio);
            }
            return ratio;
        }
    }

    /**
     * Parse an {@link AspectRatio} from a {@link String} formatted like "4:3".
     *
     * @param s The string representation of the aspect ratio
     * @return The aspect ratio
     * @throws IllegalArgumentException when the format is incorrect.
     */
    public static AspectRatio parse(String s) {
        int position = s.indexOf(':');
        if (position == -1) {
            throw new IllegalArgumentException("Malformed aspect ratio: " + s);
        }
        try {
            int x = Integer.parseInt(s.substring(0, position));
            int y = Integer.parseInt(s.substring(position + 1));
            return AspectRatio.of(x, y);
        } catch (NumberFormatException e) {
            throw new IllegalArgumentException("Malformed aspect ratio: " + s, e);
        }
    }

    private AspectRatio(int x, int y) {
        mX = x;
        mY = y;
    }

    public int getX() {
        return mX;
    }

    public int getY() {
        return mY;
    }

    public boolean matches(Size size) {
        int gcd = gcd(size.getWidth(), size.getHeight());
        int x = size.getWidth() / gcd;
        int y = size.getHeight() / gcd;
        return mX == x && mY == y;
    }

    @Override
    public boolean equals(Object o) {
        if (o == null) {
            return false;
        }
        if (this == o) {
            return true;
        }
        if (o instanceof AspectRatio) {
            AspectRatio ratio = (AspectRatio) o;
            return mX == ratio.mX && mY == ratio.mY;
        }
        return false;
    }

    @Override
    public String toString() {
        return mX + ":" + mY;
    }

    public float toFloat() {
        return (float) mX / mY;
    }

    @Override
    public int hashCode() {
        // assuming most sizes are <2^16, doing a rotate will give us perfect hashing
        return mY ^ ((mX << (Integer.SIZE / 2)) | (mX >>> (Integer.SIZE / 2)));
    }

    @Override
    public int compareTo(@NonNull AspectRatio another) {
        if (equals(another)) {
            return 0;
        } else if (toFloat() - another.toFloat() > 0) {
            return 1;
        }
        return -1;
    }

    private static int gcd(int a, int b) {
        while (b != 0) {
            int c = b;
            b = a % b;
            a = c;
        }
        return a;
    }

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeInt(mX);
        dest.writeInt(mY);
    }

    public static final Creator<AspectRatio> CREATOR
            = new Creator<AspectRatio>() {

        @Override
        public AspectRatio createFromParcel(Parcel source) {
            int x = source.readInt();
            int y = source.readInt();
            return AspectRatio.of(x, y);
        }

        @Override
        public AspectRatio[] newArray(int size) {
            return new AspectRatio[size];
        }
    };

}

记录宽高的操作的Size

    
    public class Size implements Comparable<Size> {

    private final int mWidth;
    private final int mHeight;

    /**
     * Create a new immutable Size instance.
     *
     * @param width  The width of the size, in pixels
     * @param height The height of the size, in pixels
     */
    public Size(int width, int height) {
        mWidth = width;
        mHeight = height;
    }

    public int getWidth() {
        return mWidth;
    }

    public int getHeight() {
        return mHeight;
    }

    @Override
    public boolean equals(Object o) {
        if (o == null) {
            return false;
        }
        if (this == o) {
            return true;
        }
        if (o instanceof Size) {
            Size size = (Size) o;
            return mWidth == size.mWidth && mHeight == size.mHeight;
        }
        return false;
    }


    @Override
    public int hashCode() {
        // assuming most sizes are <2^16, doing a rotate will give us perfect hashing
        return mHeight ^ ((mWidth << (Integer.SIZE / 2)) | (mWidth >>> (Integer.SIZE / 2)));
    }

    @Override
    public int compareTo(@NonNull Size another) {
        return mWidth * mHeight - another.mWidth * another.mHeight;
    }

}
    

联合AspectRatio和Size操作的集合

    
    public class SizeMap {

    private final ArrayMap<AspectRatio, SortedSet<Size>> mRatios = new ArrayMap<>();

    /**
     * Add a new {@link Size} to this collection.
     *
     * @param size The size to add.
     * @return {@code true} if it is added, {@code false} if it already exists and is not added.
     */
    public void add(Size size) {
        for (AspectRatio ratio : mRatios.keySet()) {
            if (ratio.matches(size)) {
                final SortedSet<Size> sizes = mRatios.get(ratio);
                if (sizes != null) {
                    sizes.add(size);
                }
            }
        }
        SortedSet<Size> sizes = new TreeSet<>();
        sizes.add(size);
        mRatios.put(AspectRatio.of(size.getWidth(), size.getHeight()), sizes);
    }

    SortedSet<Size> sizes(AspectRatio ratio) {
        return mRatios.get(ratio);
    }

    void clear() {
        mRatios.clear();
    }


}
    

3、总结心得

  1. 在适配的问题上来来回回磨了一个月,要耐心点再耐心点;
  2. 对Camera和SurfaceView温习一遍,温故知新。