繪製圓形surfaceview,解決預覽框的畸變問題


繪製圓形的SurfaceView

1. 首先介紹一下什麼是SurfaceView

Surface意為表層、表面,顧名思義SurfaceView就是指一個在表層的View物件。為什麼說是在表層呢,這是因為它有點特殊跟其他View不一樣,其他View是繪製在“表層”的上面,而它就是充當“表層”本身。建立SurfaceView的時候需要實現SurfaceHolder.Callback介面,它可以用來監聽SurfaceView的狀態,比如:SurfaceView的改變 、SurfaceView的建立 、SurfaceView 銷燬等,我們可以在相應的方法中做一些比如初始化的操作或者清空的操作等等。

Android系統提供了View進行繪圖處理,我們通過自定義的View可以滿足大部分的繪圖需求,但是這有個問題就是我們通常自定義的View是用於主動更新情況的,使用者無法控制其繪製的速度,由於View是通過invalidate方法通知系統去呼叫view.onDraw方法進行重繪,而Android系統是通過發出VSYNC訊號來進行螢幕的重繪,重新整理的時間是16ms,如果在16ms內View完成不了執行的操作,使用者就會看著卡頓,比如當draw方法裡執行的邏輯過多,需要頻繁重新整理的介面上,例如遊戲介面,那麼就會不斷的阻塞主執行緒,從而導致畫面卡頓。而SurfaceView相當於是另一個繪圖執行緒,它是不會阻礙主執行緒,並且它在底層實現機制中實現了雙緩衝機制。

SurfaceView最常見的應用場景就是在攝像頭的預覽,手機錄影和拍照的時候螢幕上的顯示,就是在surfaceView進行顯示的
這裡寫圖片描述
下面的程式碼是他最常見的生命週期,當SurfaceView的介面不可見時,就會呼叫surfaceDestroyed()函式來殺死本程序,當重新進入本介面時,需要重新載入。

@Override
public void surfaceCreated(SurfaceHolder holder) {
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
}

2.自己定義MySurfaceView類,繼承自SurfaceView類

先看一下效果圖
這裡寫圖片描述
我的理解是,可以把它看成是一個紅色的圖層覆蓋了原來的SurfaceView,即我們只能看到圓框中的內容

package com.example.heartratedect;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Region;
import android.hardware.Camera;
import android.util.AttributeSet;
import android.util.Log;
import android.view.SurfaceView;
/**
* 圓形SurfaceView
* 這個SurfaceView 使用時 必須設定其background,可以設定全透明背景
*/
public class MySurfaceView extends SurfaceView {
private Paint paint;
private int widthSize;
private Camera camera;
private int height; 
public MySurfaceView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
initView();
}
public MySurfaceView(Context context, AttributeSet attrs) {
super(context, attrs);
initView();
}
public MySurfaceView(Context context) {
super(context);
initView();
}
private void initView() {
this.setFocusable(true);
this.setFocusableInTouchMode(true);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
widthSize = MeasureSpec.getSize(widthMeasureSpec);
//獲取螢幕長寬比例,這樣設定不會發生畸變,千萬不要根據一個手機設定一個數
//那樣換一個手機就可能會出現顯示的比例問題
int screenWidth = CommonUtils.getScreenWidth(getContext());
int screenHeight = CommonUtils.getScreenHeight(getContext());
height=600;
//可以理解為紅色的背景蓋住了大部分的區域,我們只能看到圓框裡面的,如果還是按照原來的比例繪製surfaceview
//需要把手機拿的很遠才可以顯示出整張臉,故而我用了一個比較取巧的辦法就是,按比例縮小,試驗了很多數後,感覺0.55
//是最合適的比例
double screenWidth1= 0.55*screenWidth;
double screenHeight1= 0.55*screenHeight;
Log.e("onMeasure", "widthSize=" widthSize);
Log.e("onMeasure", "draw: widthMeasureSpec = "  screenWidth   "  heightMeasureSpec = "   screenHeight);
//繪製的輸入引數必須是整數型,做浮點型運算後為float型資料,故需要做取整操作
setMeasuredDimension((int) screenWidth1, (int) screenHeight1);
//setMeasuredDimension(widthSize, heightSize);
}
@Override
//繪製一個圓形的框,並設定圓框的座標和半徑大小
//這個繪製在16:9的手機上顯示很好,但是在更長的手機上(大於16/9)會偏上,
public void draw(Canvas canvas) {
Log.e("onDraw", "draw: test");
Path path = new Path();
//path.addCircle(widthSize / 2, height / 2, height / 2, Path.Direction.CCW);
path.addCircle(widthSize / 2, widthSize / 2, widthSize / 2, Path.Direction.CCW);
canvas.clipPath(path, Region.Op.REPLACE);
super.draw(canvas);
}
@Override
protected void onDraw(Canvas canvas) {
Log.e("onDraw", "onDraw");
super.onDraw(canvas);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
int screenWidth = CommonUtils.getScreenWidth(getContext());
int screenHeight = CommonUtils.getScreenHeight(getContext());
Log.d("screenWidth",Integer.toString(screenWidth));
Log.d("screenHeight",Integer.toString(screenHeight));
w = screenWidth;
h = screenHeight;
super.onSizeChanged(w, h, oldw, oldh);
}
}
package com.example.heartratedect;
import android.content.Context;
import android.util.DisplayMetrics;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
public class CommonUtils {
public static int getScreenWidth(Context context) {
WindowManager windowManager = (WindowManager)        context.getSystemService(Context.WINDOW_SERVICE);
DisplayMetrics outMetrics = new DisplayMetrics();// 建立了一張白紙
windowManager.getDefaultDisplay().getMetrics(outMetrics);// 給白紙設定寬高
return outMetrics.widthPixels;
}
public static int getScreenHeight(Context context) {
WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
DisplayMetrics outMetrics = new DisplayMetrics();// 建立了一張白紙
windowManager.getDefaultDisplay().getMetrics(outMetrics);// 給白紙設定寬高
return outMetrics.heightPixels;
}
}

3.接下來是MySurfaceView的例項化

例項化的方法和SurfaceView一樣

 private Camera camera;
private MySurfaceView surfaceView;
surfaceView = (MySurfaceView)CameraLayout. findViewById(R.id.camera_mysurfaceview);
cameraSurfaceHolder = surfaceView.getHolder();
cameraSurfaceHolder.addCallback(new SurfaceHolder.Callback() {
@Override
public void surfaceCreated(SurfaceHolder holder) {
try {
initView();
camera.setPreviewDisplay(holder);
isPreview = true;
camera.startPreview();
} catch (IOException e) {
e.printStackTrace();
}
cameraSurfaceHolder = holder;
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
cameraSurfaceHolder = holder;
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
releaseCamera();
}
});
// This method was deprecated in API level 11. this is ignored, this value is set automatically when needed.   
cameraSurfaceHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
private void initView(){
// 初始化攝像頭  ,設定為前置相機
camera = Camera.open(Camera.CameraInfo.CAMERA_FACING_FRONT);
Camera.Parameters parameters = camera.getParameters();
// 每秒30幀  
parameters.setPreviewFrameRate(30);
camera.setParameters(parameters);
camera.setDisplayOrientation(90);
}
/**
* 釋放攝像頭資源
*/
private void releaseCamera() {
try {
if (camera != null) {
camera.setPreviewCallback(null);
camera.stopPreview();
camera.lock();
camera.release();
camera = null;
}
} catch (Exception e) {
e.printStackTrace();
}
}

上述程式碼並沒有涉及到進度條的定義及繪製,所以不會存在顯示不協調的問題。我的程式在除錯的過程中對曲面屏非常不友好,歸結原因應該是兩側的延展和螢幕獲取的比例不一致,這個地方沒有深入研究,等實驗室買臺曲面屏的手機,做好除錯再來更新。

4.佈局檔案部分

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@ id/activity_camera"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/background_camera_activity">
//注意包名應該匯入自己的
<com.example.MySurfaceView
android:layout_marginTop="40dp"
android:layout_marginLeft="-30dp"
android:layout_centerHorizontal="true"
android:id="@ id/camera_mysurfaceview"
android:layout_width="200dp"
android:layout_height="200dp"
android:background="@drawable/circle"/>
</RelativeLayout>

同時需要給他加一個背景的圓框才行,即上面程式碼中background的設定,新建一個xml檔案命名為circle,寫下下面程式碼並放置在drawable資料夾下

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#00000000" />
<corners android:radius="90dp" />
<stroke
android:width="4dp"
android:color="#fff" />
</shape>

這樣就大工告成了,本人也是剛入門的小菜,如有啥問題,歡迎溝通,後面有啥進展我會再繼續更新。其中有些取巧的的行為:比如縮小surfaceview的比例使之正好和圓形框的直徑差不多大小,但是使用體驗上還是有些不舒服,後面還是要繼續改進