• Android 裁剪摄像头预览窗口-SurfaceView


    概述

     Android 下, 使用SurfaceView显示摄像头预览, 通常使用的是一个矩形窗口, 如果, 要使用一个圆形窗口呢?
    先上效果图
    未打开摄像头
    未打开摄像头
    打开摄像头
    打开摄像头

    实现过程

    视图布局
    在这里插入图片描述
    实现圆形裁剪的方法有很多, 最简单的, 可以在SurfaceView上方增加一个视图遮罩, 挡住不需要显示的区域即可
    遮罩的方法有一个问题:
    在这里插入图片描述
    上图中, 属于ImageView的红色区域也会被遮挡


    本文中采用的方法是:
    用一个RelativeLayout包含SurfaceView, 通过裁剪RelativeLayout来实现:

    		@Override
    		protected void dispatchDraw(Canvas canvas) {
    			//裁剪圆型画布,使SurfaceView显示圆形区域图像
    			canvas.save();
    			canvas.clipPath(path);
    			super.dispatchDraw(canvas);
    			canvas.restore();
    		}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    增加按键位置的拖动功能, 主要是为了解决验证一个问题:

    //设置输入监听, 用于拖动按键位置.
    		btn.setOnTouchListener(new View.OnTouchListener() {
    			float cx, cy, dx, dy;
    			int tx, ty;
    			@Override
    			public boolean onTouch(View v, MotionEvent event) {
    				cx = event.getRawX();
    				cy = event.getRawY();
    				switch(event.getAction()){
    					case MotionEvent.ACTION_DOWN:
    						dx = cx;
    						dy = cy;
    						tx = lpBtn.leftMargin;
    						ty = lpBtn.topMargin;
    						break;
    					case MotionEvent.ACTION_MOVE:
    						lpBtn.leftMargin = (int)(tx + (cx - dx));
    						lpBtn.topMargin = (int)(ty + (cy - dy));
    						btn.setLayoutParams(lpBtn);
    						break;
    
    					case MotionEvent.ACTION_UP:
    						//开始预览
    						cameraHelper.openCamera(false);
    						cameraHelper.startPreview(null);
    
    						//必须设置为FALSE
    						//sv.setZOrderOnTop(false);
    						rlRoot.requestLayout();
    						break;
    				}
    				return false;
    			}
    		});
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34

    当写完代码开始在设备上运行调试时, 出现了一个奇怪的问题:
    当系统显示导航栏时, 可以正常显示裁剪后的圆形, 在全屏或隐藏导航栏后, 裁剪失效了:
    在这里插入图片描述
    要解决这个问题也简单, 只需要在**XRelativeLayout(rl)**中的SurfaceView上, 覆盖一个控件, 源码中已注释

    		//MUST add view overlay
    		rl.addView(new View(this));
    
    • 1
    • 2

    当然也可以增加到rlRoot中, 从上面的动图可以看到, 裁剪的有效范围与Button的位置有关.

    参考代码

    import android.content.Context;
    import android.graphics.Canvas;
    import android.graphics.Color;
    import android.graphics.Path;
    import android.graphics.PixelFormat;
    import android.os.Bundle;
    import android.os.SystemClock;
    import android.view.MotionEvent;
    import android.view.SurfaceView;
    import android.view.View;
    import android.widget.Button;
    import android.widget.ImageView;
    import android.widget.RelativeLayout;
    import android.app.Activity;
    
    public class CameraPreview extends Activity {
    	int cameraId = 1;
    	//根控件
    	RelativeLayout rlRoot;
    	CameraHelper cameraHelper;
    	SurfaceView sv;
    	RelativeLayoutX rl;
    	@Override
    	protected void onCreate(Bundle savedInstanceState) {
    		super.onCreate(savedInstanceState);
    		rlRoot = new RelativeLayout(this);
    
    		//显示于SurfaceView 下方的ImageView
    		ImageView iv = new ImageView(this);
    		RelativeLayout.LayoutParams lpIv = new RelativeLayout.LayoutParams(UiTools.WRAP_CONTENT, UiTools.WRAP_CONTENT);
    		iv.setScaleType(ImageView.ScaleType.FIT_XY);
    		iv.setImageResource(R.drawable.ic_drawer_product_imgs);
    		iv.setOnTouchListener(new View.OnTouchListener() {
    			float cx, cy, dx, dy;
    			int tx, ty;
    			@Override
    			public boolean onTouch(View v, MotionEvent event) {
    				cx = event.getRawX();
    				cy = event.getRawY();
    				switch(event.getAction()){
    					case MotionEvent.ACTION_DOWN:
    						dx = cx;
    						dy = cy;
    						tx = lpIv.leftMargin;
    						ty = lpIv.topMargin;
    
    						break;
    					case MotionEvent.ACTION_MOVE:
    					    //同样有效
    						//btn.setTranslationX(tx + (cx - dx));
    						//btn.setTranslationY(ty + (cy - dy));
    						lpIv.leftMargin = (int)(tx + (cx - dx));
    						lpIv.topMargin = (int)(ty + (cy - dy));
    						iv.setLayoutParams(lpIv);
    						break;
    				}
    				return true;
    			}
    		});
    		rlRoot.addView(iv, lpIv);
    
    		//SurfaceView
    		rl = new RelativeLayoutX(this);
    		sv = new SurfaceView(this);
    		//当未打开摄像头开始预览时, 显示透明(不设置则显示黑色)
    		//sv.setZOrderOnTop(true);
    		//sv.getHolder().setFormat(PixelFormat.TRANSLUCENT);
    		rl.addView(sv, new RelativeLayout.LayoutParams(UiTools.MATCH_PARENT, UiTools.MATCH_PARENT));
    		cameraHelper = new CameraHelper(camBase.getParameters(cameraId), sv, camBase);
    		rlRoot.addView(rl);
    
    		//MUST add view overlay
    		//rl.addView(new View(this));
    		RelativeLayout.LayoutParams lpBtn = new RelativeLayout.LayoutParams(UiTools.WRAP_CONTENT, UiTools.WRAP_CONTENT);
    		Button btn = new Button(this);
    		btn.setText("Show");
    		//设置输入监听, 用于拖动按键位置.
    		btn.setOnTouchListener(new View.OnTouchListener() {
    			float cx, cy, dx, dy;
    			int tx, ty;
    			@Override
    			public boolean onTouch(View v, MotionEvent event) {
    				cx = event.getRawX();
    				cy = event.getRawY();
    				switch(event.getAction()){
    					case MotionEvent.ACTION_DOWN:
    						dx = cx;
    						dy = cy;
    						tx = lpBtn.leftMargin;
    						ty = lpBtn.topMargin;
    						break;
    					case MotionEvent.ACTION_MOVE:
    						lpBtn.leftMargin = (int)(tx + (cx - dx));
    						lpBtn.topMargin = (int)(ty + (cy - dy));
    						btn.setLayoutParams(lpBtn);
    						break;
    
    					case MotionEvent.ACTION_UP:
    						//开始预览
    						cameraHelper.openCamera(false);
    						cameraHelper.startPreview(null);
    
    						//必须设置为FALSE
    						//sv.setZOrderOnTop(false);
    						rlRoot.requestLayout();
    						break;
    				}
    				return false;
    			}
    		});
    		rlRoot.addView(btn, lpBtn);
    
    		setContentView(rlRoot);
    	}
    
    	@Override
    	protected void onDestroy() {
    		super.onDestroy();
    		cameraHelper.stopPreview();
    	}
    
    	static class RelativeLayoutX extends RelativeLayout{
    		public RelativeLayoutX(Context context) {
    			super(context);
    		}
    
    		Path path = new Path();
    		@Override
    		protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    			super.onSizeChanged(w, h, oldw, oldh);
    			path.reset();
    			path.addCircle(w/2f, h/2f, Math.min(w, h)/2f, Path.Direction.CW);
    		}
    
    		@Override
    		protected void dispatchDraw(Canvas canvas) {
    			//裁剪圆型画布,使SurfaceView显示圆形区域图像
    			canvas.save();
    			canvas.clipPath(path);
    			super.dispatchDraw(canvas);
    			canvas.restore();
    		}
    	}
    
    	//非关键代码, 主要用于返回打开摄像头预览的参数
    	CameraHelper.CameraBase camBase = new CameraHelper.CameraBaseSimple(){
    		@Override
    		public CameraHelper.CameraParams getParameters(int camIndex) {
    			return new CameraHelper.CameraParams(cameraId, 640, 480, 30, 0, 0);
    		}
    
    		@Override
    		public boolean sameParams(CameraHelper.CameraParams cp) {
    			return true;
    		}
    
    		@Override
    		public void onPreviewStarted(int camIndex) {}
    
    		@Override
    		public void onCameraError(int error) {}
    	};
    
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159
    • 160
    • 161
    • 162
    • 163
    • 164
    • 165

    PS
    虽然提出了需求, 也实现了想要的效果, 却也留下了一个问题, 问题虽然解决, 但却找不到具体的原因!

    1. 浮动窗的情况未测试过
    2. Dialog或其它弹出窗口未测试过
    3. setZOrderOnTop(true)的情况未有好的解决办法

    参考

    解决SurfaceView渲染的各种疑难杂症
    android 圆形相机预览拍照_Android相机(摄像头)圆形预览窗口,圆形SurfaceView

  • 相关阅读:
    docker安装gitlab 并dump出表结构
    算法数据结构体系学习 第十一节
    2022年12月 Python(一级)真题解析#中国电子学会#全国青少年软件编程等级考试
    百度工程师眼中的云原生可观测性追踪技术
    关系数据库:mysql
    【Java】面向对象程序设计 错题本
    飞桨paddlespeech语音唤醒推理C实现
    2023/09/15 qt day2
    node.js基础学习
    ubuntu18.04升级Cmake
  • 原文地址:https://blog.csdn.net/ansondroider/article/details/126268640