天天看點

TextureView實作相機預覽拍照功能1. TextureView簡介2. 代碼執行個體3. 實作過程中遇到的坑

TextureView實作相機預覽拍照功能

  • 1. TextureView簡介
  • 2. 代碼執行個體
      • 2.1 自定義CameraTextureView控件,實作SurfaceTextureListener,封裝TextureView的初始化和銷毀功能
      • 2.2 Camera的處理封裝到CameraManager類中
      • 2.3 建立TakePictureActivity實作拍照頁面
      • 2.4 AndroidManifest.xml中添加相機權限
  • 3. 實作過程中遇到的坑
      • 3.1 在非全屏預覽頁面中出現畫面擠壓變形
      • 3.2 拍照後得到的圖檔尺寸與預覽時不一緻

1. TextureView簡介

TextureView 是一個顯示資料流的UI控件。它是View的子類,可以當做普通的View控件使用,在布局、動畫和變換(平移、縮放、旋轉等)中非常友善,

主要用于2種場景

1.相機預覽 2:視訊播放

這篇文章主要講述TextureView相機預覽拍照功能的實作

2. 代碼執行個體

2.1 自定義CameraTextureView控件,實作SurfaceTextureListener,封裝TextureView的初始化和銷毀功能

public class CameraTextureView extends TextureView implements TextureView.SurfaceTextureListener {
    private static final String TAG = "CameraTextureView";
    private SurfaceTexture mSurface;
    private int mWidth;
    private int mHeight;

    public CameraTextureView(Context context) {
        this(context, null);
    }

    public CameraTextureView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public CameraTextureView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        this.setSurfaceTextureListener(this);
    }

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    public CameraTextureView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        this.setSurfaceTextureListener(this);
    }

    @Override
    public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
        mSurface = surface;
        mWidth = width;
        mHeight = height;
        //開啟相機是耗時操作,此處異步處理
        new Thread(new OpenCameraRunnable()).start();
    }

    @Override
    public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
        CameraManager.getInstance().stopCamera();
        return true;
    }

    @Override
    public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {
    }

    @Override
    public void onSurfaceTextureUpdated(SurfaceTexture surface) {
    }

    public SurfaceTexture getSurfaceTexture() {
        return mSurface;
    }

    private class OpenCameraRunnable implements Runnable {

        @Override
        public void run() {
            CameraManager.getInstance().openCamera(getContext(), mSurface, mWidth, mHeight);
        }
    }
}  
           

2.2 Camera的處理封裝到CameraManager類中

public class CameraManager {
    private static CameraManager mCameraManager;
    public static final int LEFT = 0;
    public static final int TOP = 1;
    public static final int RIGHT = 2;
    public static final int BOTTOM = 3;
    private OnCameraActionCallback mOnCameraActionCallback;
    private Camera mCamera;
    private ToneGenerator mToneGenerator;
    private Context mContext;
    private byte[] mCameraData;//拍照傳回的圖像資料
    private boolean isPreviewing = false;
    private int mCurrentCameraFacing;//目前攝像頭
    private int mDeviceOrientation = TOP;

    public static synchronized CameraManager getInstance() {
        if (mCameraManager == null) {
            mCameraManager = new CameraManager();
        }
        return mCameraManager;
    }

    /**
     * 打開Camera
     */
    public void openCamera(Context context, SurfaceTexture surface, int width, int height) {
        mContext = context;
        mCamera = Camera.open();
        mCurrentCameraFacing = 0;
        startPreview(surface, width, height);
    }

    /**
     * 使用TextureView預覽Camera
     *
     * @param surface
     */
    public void startPreview(SurfaceTexture surface, int width, int height) {
        if (isPreviewing) {
            mCamera.stopPreview();
            return;
        }
        if (mCamera != null) {
            try {
                mCamera.setPreviewTexture(surface);
            } catch (IOException e) {
                e.printStackTrace();
            }
            initCamera(width, height);
        }
    }

    /**
     * 停止預覽,釋放Camera
     */
    public void stopCamera() {
        if (mCamera != null) {
            mCamera.setPreviewCallback(null);
            mCamera.stopPreview();
            isPreviewing = false;
            mCamera.release();
            mCamera = null;
            mContext = null;
        }
    }

   private void initCamera(int width, int height) {
        if (mCamera != null) {
            Camera.Parameters params = mCamera.getParameters();
            params.setPictureFormat(PixelFormat.JPEG);//設定拍照後存儲的圖檔格式
            //設定PreviewSize和PictureSize
            Size pictureSize = getOptimalSize(params.getSupportedPictureSizes(), width, height);
            params.setPictureSize(pictureSize.width, pictureSize.height);
            Size previewSize = getOptimalSize(params.getSupportedPreviewSizes(), width, height);
            params.setPreviewSize(previewSize.width, previewSize.height);

            mCamera.setDisplayOrientation(90);

            List<String> focusModes = params.getSupportedFocusModes();
            if (focusModes.contains(Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO)) {
                params.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO);
            }
            mCamera.setParameters(params);
            mCamera.startPreview();//開啟預覽

            isPreviewing = true;
        }
    }

    /**
     * 拍照
     */
    public void takePicture(OnCameraActionCallback callback) {
        mOnCameraActionCallback = callback;
        if (isPreviewing && (mCamera != null)) {
            mCamera.takePicture(mShutterCallback, null, mJpegPictureCallback);
        }
    }

    /**
     * 儲存照片
     */
    public String save() {
        if (mCameraData != null) {
            Bitmap bitmap = BitmapFactory.decodeByteArray(mCameraData, 0, mCameraData.length);//data是位元組資料,将其解析成位圖
            //儲存圖檔到sdcard
            //設定FOCUS_MODE_CONTINUOUS_VIDEO)之後,myParam.set("rotation", 90)失效。
            //圖檔竟然不能旋轉了,故這裡要旋轉下
            bitmap = BitmapUtils.rotateBitmap(bitmap, 90.0f);
            return BitmapUtils.saveBitmap(mContext, bitmap);
        }
        return null;
    }

    public void cancel() {
        //再次進入預覽
        mCamera.startPreview();
        isPreviewing = true;
    }

    public interface OnCameraActionCallback {
        void onTakePictureComplete(Bitmap bitmap);
    }

    //快門按下的回調,在這裡我們可以設定類似播放“咔嚓”聲之類的操作。預設的就是咔嚓。
    private Camera.ShutterCallback mShutterCallback = new Camera.ShutterCallback() {

        public void onShutter() {
            // TODO Auto-generated method stub
            if (mToneGenerator == null) {
                //發出提示使用者的聲音
                mToneGenerator = new ToneGenerator(AudioManager.STREAM_MUSIC,
                        ToneGenerator.MAX_VOLUME);
            }
            mToneGenerator.startTone(ToneGenerator.TONE_PROP_BEEP2);
        }
    };

    //對jpeg圖像資料的回調,最重要的一個回調
    private Camera.PictureCallback mJpegPictureCallback = new Camera.PictureCallback() {

        public void onPictureTaken(byte[] data, Camera camera) {
            //mCameraData = data;
            if (mOnCameraActionCallback != null) {
                Bitmap bitmap = BitmapFactory.decodeByteArray(data, 0, data.length);//data是位元組資料,将其解析成位圖
                if (mCurrentCameraFacing == Camera.CameraInfo.CAMERA_FACING_BACK) {
                    if (mDeviceOrientation == TOP) {
                        bitmap = BitmapUtils.rotateBitmap(bitmap, 90.0f);
                    } else if (mDeviceOrientation == RIGHT) {
                        bitmap = BitmapUtils.rotateBitmap(bitmap, 180.0f);
                    } else if (mDeviceOrientation == BOTTOM) {
                        bitmap = BitmapUtils.rotateBitmap(bitmap, 270.0f);
                    }
                } else if (mCurrentCameraFacing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
                    if (mDeviceOrientation == TOP) {
                        bitmap = BitmapUtils.rotateBitmap(bitmap, 270.0f);
                    } else if (mDeviceOrientation == RIGHT) {
                        bitmap = BitmapUtils.rotateBitmap(bitmap, 180.0f);
                    } else if (mDeviceOrientation == BOTTOM) {
                        bitmap = BitmapUtils.rotateBitmap(bitmap, 90.0f);
                    }
                }
                mOnCameraActionCallback.onTakePictureComplete(bitmap);
            }
        }
    };
}
           

2.3 建立TakePictureActivity實作拍照頁面

public class TakePictureActivity extends AppCompatActivity implements View.OnClickListener,
        CameraManager.OnCameraActionCallback {
    private CameraTextureView mTextureView;
    private ImageView mPicture;
    private View mTakePicture;
    private View mCameraSave;

    private Bitmap mPictureBitmap;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_take_picture);

        initView();
    }

    private void initView() {
        mTextureView = findViewById(R.id.texture_view);
        mPicture = findViewById(R.id.iv_camera_picture);
        mTakePicture = findViewById(R.id.iv_take_picture);
        mCameraSave = findViewById(R.id.iv_camera_save);

        mTakePicture.setOnClickListener(this);
        mCameraSave.setOnClickListener(this);

        findViewById(R.id.iv_camera_cancel).setOnClickListener(this);
    }

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.iv_take_picture:
                CameraManager.getInstance().takePicture(this);
                break;
            case R.id.iv_camera_cancel:
                CameraManager.getInstance().cancel();
                setResultState(false);
                break;
            case R.id.iv_camera_save:
                //todo 此處可以實作儲存圖檔到相冊功能
                break;
            default:
                break;
        }
    }

    private void setResultState(boolean state) {
        mTextureView.setTouchEnable(!state);
        if (state) {
            mTakePicture.setVisibility(View.GONE);
            ((View) mCameraSave.getParent()).setVisibility(View.VISIBLE);
            mTextureView.setVisibility(View.GONE);
            mPicture.setVisibility(View.VISIBLE);
        } else {
            mTakePicture.setVisibility(View.VISIBLE);
            ((View) mCameraSave.getParent()).setVisibility(View.GONE);
            mTextureView.setVisibility(View.VISIBLE);
            mPicture.setVisibility(View.GONE);
        }
    }

    @Override
    public void onTakePictureComplete(Bitmap bitmap) {
        if (mPictureBitmap != null) {
            mPictureBitmap.recycle();
        }
        mPictureBitmap = bitmap;
        mPicture.setImageBitmap(bitmap);
        setResultState(true);
    }
}
           

布局檔案如下:

<?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="@android:color/black">

    <com.owner.camera.camera.CameraTextureView
        android:id="@+id/texture_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

    <ImageView
        android:id="@+id/iv_camera_picture"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom"
        android:layout_marginBottom="48dp"
        android:layout_marginLeft="36dp"
        android:layout_marginRight="36dp"
        android:visibility="visible">

        <ImageView
            android:id="@+id/iv_camera_cancel"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_vertical"
            android:padding="12dp"
            android:src="@android:drawable/ic_menu_close_clear_cancel" />

        <ImageView
            android:id="@+id/iv_camera_save"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="right|center_vertical"
            android:padding="12dp"
            android:src="@android:drawable/ic_menu_save" />
    </FrameLayout>

    <ImageView
        android:id="@+id/iv_take_picture"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal|bottom"
        android:layout_marginBottom="32dp"
        android:layout_marginLeft="36dp"
        android:layout_marginRight="36dp"
        android:src="@android:drawable/ic_menu_camera" />
</FrameLayout>
           

2.4 AndroidManifest.xml中添加相機權限

<!-- 多媒體相關 -->
    <uses-permission android:name="android.permission.CAMERA" />
    <uses-feature android:name="android.hardware.camera" />
    <uses-feature android:name="android.hardware.camera.autofocus" />
           

TakePictureActivity預設開啟了硬體加速,這也是TextureView生效的前提條件

android:hardwareAccelerated="true"
           

至此基于TextureView的相機拍照預覽功能已基本實作,可根據此Demo定制自己的相機,添加更豐富的功能。

3. 實作過程中遇到的坑

3.1 在非全屏預覽頁面中出現畫面擠壓變形

原因是UI中CameraTextureView的寬高比與相機提供的寬高比不一緻,是以在設定控件寬高時一定要按照标準比例設計,Camera系統提供了多種預覽尺寸(使用

getSupportedPreviewSizes()

獲得),根據需要選擇其一即可,你也可以在運作時根據選取的預覽尺寸動态設定控件寬高

3.2 拍照後得到的圖檔尺寸與預覽時不一緻

原因與3.1相似,是因為PictureSize與PreviewSize不一緻導緻的,設定成一樣的比例即可

總結:TextureView 與 PreViewSize 以及 PictureSize 這三者的寬高比應盡量相同,以免造成視覺上的不一緻。前兩者決定預覽時我們所看見的效果,而 PictureSize 隻最終決定生成的位圖檔案的大小。