Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> Android開發 >> 關於android開發 >> Android 手把手帶你玩轉自定義相機

Android 手把手帶你玩轉自定義相機

編輯:關於android開發

Android 手把手帶你玩轉自定義相機


概述

相機幾乎是每個APP都要用到的功能,萬一老板讓你定制相機方不方?反正我是有點方。關於相機的兩天奮斗總結免費送給你。

啟動相機的兩種方式

1.直接啟動系統相機

  Intent intent = new Intent();  
  intent.setAction(MediaStore.ACTION_IMAGE_CAPTURE);  
  startActivity(intent);

或者指定返回圖片的名稱mCurrentPhotoFile

        Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
        intent.putExtra(MediaStore.EXTRA_OUTPUT,Uri.fromFile(mCurrentPhotoFile));
        startActivityForResult(intent, CAMERA_WITH_DATA);

2.自定義啟動相機
今天以第二種為例。效果圖如下

demo

自定義相機的一般步驟

創建顯示相機畫面的布局,Android已經為我們選定好SurfaceView 通過SurfaceView#getHolder()獲得鏈接Camera和SurfaceView的SurfaceHolder Camame.open()打開相機 通過SurfaceHolder鏈接Camera和SurfaceView

一般步驟的代碼演示

public class CameraSurfaceView extends SurfaceView implements SurfaceHolder.Callback, Camera.AutoFocusCallback {

    private static final String TAG = "CameraSurfaceView";

    private Context mContext;
    private SurfaceHolder holder;
    private Camera mCamera;

    private int mScreenWidth;
    private int mScreenHeight;

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

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

    public CameraSurfaceView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mContext = context;
        getScreenMetrix(context);
        initView();
    }

    private void getScreenMetrix(Context context) {
        WindowManager WM = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
        DisplayMetrics outMetrics = new DisplayMetrics();
        WM.getDefaultDisplay().getMetrics(outMetrics);
        mScreenWidth = outMetrics.widthPixels;
        mScreenHeight = outMetrics.heightPixels;
    }

    private void initView() {
        holder = getHolder();//獲得surfaceHolder引用
        holder.addCallback(this);
        holder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);//設置類型
    }

    @Override
    public void surfaceCreated(SurfaceHolder holder) {
        Log.i(TAG, "surfaceCreated");
        if (mCamera == null) {
            mCamera = Camera.open();//開啟相機
            try {
                mCamera.setPreviewDisplay(holder);//攝像頭畫面顯示在Surface上
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

    }

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

    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
        Log.i(TAG, "surfaceDestroyed");
        mCamera.stopPreview();//停止預覽
        mCamera.release();//釋放相機資源
        mCamera = null;
        holder = null;
    }

    @Override
    public void onAutoFocus(boolean success, Camera Camera) {
        if (success) {
            Log.i(TAG, "onAutoFocus success="+success);
        }
    }
}

添加相機和自動聚焦限權


將CameraSurfaceView放在布局文件中,這裡建議最外層為FrameLayout,後面會用到。如此,我們便有了一個沒有照相功能的相機。初次之外,仔細觀察相機顯示畫面,圖片是不是變形嚴重?那是因為我們還沒有為相機設置各種參數。在預覽前要設置攝像頭的分辨率、預覽分辨率和圖片分辨率的寬高比保持一致。這樣圖片才不會變形。這是個比較難以理解的部分,想深刻理解還需讀者自己動手去實踐。

   private void setCameraParams(Camera camera, int width, int height) {
        Log.i(TAG,"setCameraParams  width="+width+"  height="+height);
        Camera.Parameters parameters = mCamera.getParameters();
        // 獲取攝像頭支持的PictureSize列表
        List pictureSizeList = parameters.getSupportedPictureSizes();
        for (Camera.Size size : pictureSizeList) {
            Log.i(TAG, "pictureSizeList size.width=" + size.width + "  size.height=" + size.height);
        }
        /**從列表中選取合適的分辨率*/
        Camera.Size picSize = getProperSize(pictureSizeList, ((float) height / width));
        if (null == picSize) {
            Log.i(TAG, "null == picSize");
            picSize = parameters.getPictureSize();
        }
        Log.i(TAG, "picSize.width=" + picSize.width + "  picSize.height=" + picSize.height);
         // 根據選出的PictureSize重新設置SurfaceView大小
        float w = picSize.width;
        float h = picSize.height;
        parameters.setPictureSize(picSize.width,picSize.height);
        this.setLayoutParams(new FrameLayout.LayoutParams((int) (height*(h/w)), height));

        // 獲取攝像頭支持的PreviewSize列表
        List previewSizeList = parameters.getSupportedPreviewSizes();

        for (Camera.Size size : previewSizeList) {
            Log.i(TAG, "previewSizeList size.width=" + size.width + "  size.height=" + size.height);
        }
        Camera.Size preSize = getProperSize(previewSizeList, ((float) height) / width);
        if (null != preSize) {
            Log.i(TAG, "preSize.width=" + preSize.width + "  preSize.height=" + preSize.height);
            parameters.setPreviewSize(preSize.width, preSize.height);
        }

        parameters.setJpegQuality(100); // 設置照片質量
        if (parameters.getSupportedFocusModes().contains(android.hardware.Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE)) {
            parameters.setFocusMode(android.hardware.Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE);// 連續對焦模式
        }

        mCamera.cancelAutoFocus();//自動對焦。
        mCamera.setDisplayOrientation(90);// 設置PreviewDisplay的方向,效果就是將捕獲的畫面旋轉多少度顯示
        mCamera.setParameters(parameters);

    }

    /**
     * 從列表中選取合適的分辨率
     * 默認w:h = 4:3
     *

注意:這裡的w對應屏幕的height * h對應屏幕的width

*/ private Camera.Size getProperSize(List pictureSizeList, float screenRatio) { Log.i(TAG, "screenRatio=" + screenRatio); Camera.Size result = null; for (Camera.Size size : pictureSizeList) { float currentRatio = ((float) size.width) / size.height; if (currentRatio - screenRatio == 0) { result = size; break; } } if (null == result) { for (Camera.Size size : pictureSizeList) { float curRatio = ((float) size.width) / size.height; if (curRatio == 4f / 3) {// 默認w:h = 4:3 result = size; break; } } } return result; }

進去的是屏幕寬高,出來的是調整好了的參數。在surfaceChanged方法中執行mCamera.startPreview(); 前調用setCameraParams(mCamera, mScreenWidth, mScreenHeight); 就可以了。最後要在AndroidManifest.xml裡設置activity的方向android:screenOrientation="portrait"代碼裡有很多注釋,其中也有我自己調試時候的Log,大家可以自己調試下,看看不同參數的效果。昨天調參數搞到一點多,都在折騰這個函數。唉,一把辛酸淚。
身為一個相機,居然不能照相?真是太丟臉了!下面給我們的相機添加上照相的功能。照相核心代碼就一句:mCamera.takePicture(null, null, jpeg);
可以看到takePicture方法有三個參數,分別是ShutterCallback、PictureCallback和PictureCallback。這裡我們只用了PictureCallback

    // 拍照瞬間調用
    private Camera.ShutterCallback shutter = new Camera.ShutterCallback() {
        @Override
        public void onShutter() {
            Log.i(TAG,"shutter");
        }
    };

    // 獲得沒有壓縮過的圖片數據
    private Camera.PictureCallback raw = new Camera.PictureCallback() {

        @Override
        public void onPictureTaken(byte[] data, Camera Camera) {
            Log.i(TAG, "raw");

        }
    };

    //創建jpeg圖片回調數據對象
    private Camera.PictureCallback jpeg = new Camera.PictureCallback() {

        @Override
        public void onPictureTaken(byte[] data, Camera Camera) {
            BufferedOutputStream bos = null;
            Bitmap bm = null;
            try {
                // 獲得圖片
                bm = BitmapFactory.decodeByteArray(data, 0, data.length);
                if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
                    Log.i(TAG, "Environment.getExternalStorageDirectory()="+Environment.getExternalStorageDirectory());
                    String filePath = "/sdcard/dyk"+System.currentTimeMillis()+".jpg";//照片保存路徑
                    File file = new File(filePath);
                    if (!file.exists()){
                        file.createNewFile();
                    }
                    bos = new BufferedOutputStream(new FileOutputStream(file));
                    bm.compress(Bitmap.CompressFormat.JPEG, 100, bos);//將圖片壓縮到流中

                }else{
                    Toast.makeText(mContext,"沒有檢測到內存卡", Toast.LENGTH_SHORT).show();
                }
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                try {
                    bos.flush();//輸出
                    bos.close();//關閉
                    bm.recycle();// 回收bitmap空間
                    mCamera.stopPreview();// 關閉預覽
                    mCamera.startPreview();// 開啟預覽
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

        }
    };

在jpeg的onPictureTaken裡。我們將存儲照片信息的byte[] data解析成bitmap,然後轉換成JPG格式的圖片保存在SD卡中。注意finally中最後兩句mCamera.stopPreview();// 關閉預覽 mCamera.startPreview();// 開啟預覽 上文也提到:當調用camera.takePiture方法後,camera關閉了預覽,這時需要調用startPreview()來重新開啟預覽。如果不再次開啟預覽,則會一直停留在拍攝照片畫面。為了方便外部調用拍照。這裡我暴露了一個方法供外部拍照。

    public void takePicture(){
        //設置參數,並拍照
        setCameraParams(mCamera, mScreenWidth, mScreenHeight);
        // 當調用camera.takePiture方法後,camera關閉了預覽,這時需要調用startPreview()來重新開啟預覽
        mCamera.takePicture(null, null, jpeg);


    }

在布局文件中添加一個Button,點擊Button執行takePicture()方法。不要忘了添加寫SD卡限權

至此,一個具有照相並保存拍攝圖片功能的相機就做出來了。But,我們就此滿足了嗎?要是為了這些簡單的功能我也不會寫這篇博客。這只是個開始

真正的開始

昨天看見別的APP在照相的時候,屏幕上居然可以顯示像效果圖那樣的框框啦、輔助點啦、圖片bulabulabula~。在網上搜索一番實現方式,再加上一些自己的理解,構成了這篇博客。
上文布局文件一直沒有貼,現在貼出來大家先掃一眼,有些控件會在接下來展示


<framelayout android:layout_height="match_parent" android:layout_width="match_parent" xmlns:android="http://schemas.android.com/apk/res/android">

    

    

    

        </framelayout>

布局文件的最外層是個FrameLayout,我們知道FrameLayout是自帶覆蓋效果的。由來這個思路接下來就很簡單了。編程重要的是思想,思想有了,其余的就剩具體的實現細節。

自定義邊邊框框

為了和CameraSurfaceView區分開,再自定義一個RectOnCamera專門用來畫邊邊框框這些東西。這樣做還一個好處是方便維護,不至於將所有東西都放在一個View中。

RectOnCamera

package com.dyk.cameratest.view;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Point;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.WindowManager;

/**
 * Created by dyk on 2016/4/7.
 */
public class RectOnCamera extends View {
    private static final String TAG = "CameraSurfaceView";
    private int mScreenWidth;
    private int mScreenHeight;
    private Paint mPaint;
    private RectF mRectF;
    // 圓
    private Point centerPoint;
    private int radio;

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

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

    public RectOnCamera(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        getScreenMetrix(context);
        initView(context);
    }

    private void getScreenMetrix(Context context) {
        WindowManager WM = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
        DisplayMetrics outMetrics = new DisplayMetrics();
        WM.getDefaultDisplay().getMetrics(outMetrics);
        mScreenWidth = outMetrics.widthPixels;
        mScreenHeight = outMetrics.heightPixels;
    }

    private void initView(Context context) {
        mPaint = new Paint();
        mPaint.setAntiAlias(true);// 抗鋸齒
        mPaint.setDither(true);// 防抖動
        mPaint.setColor(Color.RED);
        mPaint.setStrokeWidth(5);
        mPaint.setStyle(Paint.Style.STROKE);// 空心
        int marginLeft = (int) (mScreenWidth*0.15);
        int marginTop = (int) (mScreenHeight * 0.25);
        mRectF = new RectF(marginLeft, marginTop, mScreenWidth - marginLeft, mScreenHeight - marginTop);

        centerPoint = new Point(mScreenWidth/2, mScreenHeight/2);
        radio = (int) (mScreenWidth*0.1);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        mPaint.setColor(Color.RED);
        canvas.drawRect(mRectF, mPaint);
        mPaint.setColor(Color.WHITE);
        Log.i(TAG, "onDraw");
        canvas.drawCircle(centerPoint.x,centerPoint.y, radio,mPaint);// 外圓
        canvas.drawCircle(centerPoint.x,centerPoint.y, radio - 20,mPaint); // 內圓
    }
}

這裡簡單的畫了一個類似二維碼掃描的框框,還有一個類似聚焦的內外圓。那麼問題來了,聚焦的內外圓要隨著手指滑而改變位置,而且要有聚焦的效果。可又和具有聚焦功能的CameraSurfaceView不是同一個類,不僅如此聚焦內外圓還完全覆蓋了CameraSurfaceView。要處理這種問題,需要接口回調。這就是思想下面的細節。現在雖然確定接口回調,但還有一個問題,CameraSurfaceView類和RectOnCamera類中都沒有對方的對象或者引用。沒錯,通過共同持有RectOnCamera和CameraSurfaceView的Activity可以實現此功能。下面是具體的實現方法。

動起來

首先,想要隨著手指的滑動而改變RectOnCamera的位置肯定是要復寫onTouchEvent()方法

   @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
            case MotionEvent.ACTION_MOVE:
            case MotionEvent.ACTION_UP:
                int x = (int) event.getX();
                int y = (int) event.getY();
                centerPoint = new Point(x, y);
                invalidate();
                return true;
        }
        return true;
    }

其次,定義回調接口

 private IAutoFocus mIAutoFocus;

    /** 聚焦的回調接口 */
    public interface  IAutoFocus{
        void autoFocus();
    }

    public void setIAutoFocus(IAutoFocus mIAutoFocus) {
        this.mIAutoFocus = mIAutoFocus;
    }

在onTouchEvent()中return前加入

  if (mIAutoFocus != null){
      mIAutoFocus.autoFocus();
  }

至此我們的回調接口已經定義好了,此時還需要CameraSurfaceView暴露一個聚焦方法,以便Activity調用

    public void setAutoFocus(){
        mCamera.autoFocus(this);
    }

准備工作已經全部完成,下面請看Activity的具體實現:

public class MainActivity extends Activity implements View.OnClickListener,RectOnCamera.IAutoFocus{

    private CameraSurfaceView mCameraSurfaceView;
    private RectOnCamera mRectOnCamera;
    private Button takePicBtn;

    private boolean isClicked;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        requestWindowFeature(Window.FEATURE_NO_TITLE);
        // 全屏顯示
        getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,WindowManager.LayoutParams.FLAG_FULLSCREEN);
        setContentView(R.layout.activity_main);
        mCameraSurfaceView = (CameraSurfaceView) findViewById(R.id.cameraSurfaceView);
        mRectOnCamera = (RectOnCamera) findViewById(R.id.rectOnCamera);
        takePicBtn= (Button) findViewById(R.id.takePic);
        mRectOnCamera.setIAutoFocus(this);
        takePicBtn.setOnClickListener(this);

    }


    @Override
    public void onClick(View v) {
        switch (v.getId()){
            case R.id.takePic:
                mCameraSurfaceView.takePicture();
                break;
            default:
                break;
        }
    }


    @Override
    public void autoFocus() {
        mCameraSurfaceView.setAutoFocus();
    }
}

可以看到,MainActivity實現了IAutoFocus接口,並且在復寫的IAutoFocus#autoFocus()方法中,調用了CameraSurfaceView暴露出來的方法setAutoFocus()。至此,在RectOnCamera每次的滑動過程中都會改變聚焦內外圓的位置,還會增加聚焦功能。一心二用甚至一心多用豈不是更好。

結束語

在經歷兩次沒保存斷電和一次CSDN服務器錯誤內容丟失之後,終究還是完成了這篇博客,實屬不易。感謝能聽我啰嗦到結尾~

PS:此Demo界面並沒有做的很精致,只是提供了一種思路。按照此思路能做出比較華麗的效果,授人以魚不如授人以漁。

 

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved