Android 水印相机开发

水印相机是自定义相机的一种,实现方法有很多,我看了很多别人的做的很漂亮,我做的就很普通了,不过总算是实现了拍照加水印的功能。

我这边用到了SurfaceView,有人没用这个也做出来水印相机,个人觉得还是SurfaceView更方便一点(不接受反驳)。

先看看效果:
效果图
原图太大,我做了压缩,所以动图显得模糊。

第一步,我们想一进入就打开相机预览,这个怎么做呢?
相机功能由android.hardware.Camera类实现,但是需要有一个预览载体,这里就用SurfaceView,而且需要辅助类SurfaceHolder,首先,我们的 Activity 要实现SurfaceHolder.Callback接口:

1
public class WaterCameraActivity extends AppCompatActivity implements SurfaceHolder.Callback

第二步,关联SurfaceHolder

1
2
3
4
5
6
7
8
private SurfaceView mSv;
private SurfaceHolder mSurfaceHolder;
mSurfaceHolder = mSv.getHolder();
mSurfaceHolder.setKeepScreenOn(true);
mSurfaceHolder.setFormat(PixelFormat.TRANSPARENT);
mSurfaceHolder.addCallback(this);
// 为了实现照片预览功能,需要将SurfaceHolder的类型设置为PUSH,这样画图缓存就由Camera类来管理,画图缓存是独立于Surface的
mSurfaceHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);

实现SurfaceHolder.Callback接口有三个方法需要重写:

1
2
3
4
5
6
7
8
9
10
11
12
@Override
public void surfaceCreated(SurfaceHolder holder) {

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

}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {

}

只要SurfaceView显示,就会调用surfaceCreated(),不显示就会调用surfaceDestroyed()。因此可以在surfaceCreated()中初始化相机,并展示预览界面;在surfaceDestroyed()中释放相机资源。
第三步,初始化相机

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
mCamera = Camera.open(0);//0-后摄像头,1-前摄像头
Camera.getCameraInfo(0, cameraInfo);
Camera.Parameters parameters = mCamera.getParameters();
// 设置图片格式
parameters.setPictureFormat(ImageFormat.JPEG);
// 设置照片质量
parameters.setJpegQuality(100);
// 首先获取系统设备支持的所有颜色特效,如果设备不支持颜色特性将返回一个null, 如果有符合我们的则设置
List<String> colorEffects = parameters.getSupportedColorEffects();
Iterator<String> colorItor = colorEffects.iterator();
while (colorItor.hasNext()) {
String currColor = colorItor.next();
if (currColor.equals(Camera.Parameters.EFFECT_SOLARIZE)) {
parameters.setColorEffect(Camera.Parameters.EFFECT_AQUA);
break;
}
}
// 获取对焦模式
List<String> focusModes = parameters.getSupportedFocusModes();
if (focusModes.contains(Camera.Parameters.FOCUS_MODE_AUTO)) {
// 设置自动对焦
parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO);
}

// 设置闪光灯自动开启
List<String> flashModes = parameters.getSupportedFlashModes();
if (flashModes.contains(Camera.Parameters.FLASH_MODE_AUTO)) {
// 自动闪光
parameters.setFlashMode(Camera.Parameters.FLASH_MODE_AUTO);
}
mCamera.setDisplayOrientation(setCameraDisplayOrientation());
// 设置显示
mCamera.setPreviewDisplay(mSurfaceHolder);

List<Camera.Size> photoSizes = parameters.getSupportedPictureSizes();//获取系统可支持的图片尺寸
int width = 0, height = 0;
for (Camera.Size size : photoSizes) {
if (size.width > width) width = size.width;
if (size.height > height) height = size.height;
}
parameters.setPictureSize(width, height);
// 设置完成需要再次调用setParameter方法才能生效
mCamera.setParameters(parameters);
// 开始预览
mCamera.startPreview();

这样就可以预览相机界面了,多说一点,我是在小米 8 手机调试的,照片很清晰,拍出来的照片有 8M 多大,但是换成荣耀 8,图片只有几十 Kb,很不清楚。单步调试的时候可以发现,parameters.getSupportedPictureSizes()这里获取的集合,小米和荣耀排序方式是不一样的,一个是清晰度由低到高,另一个由高到低。所以才改成上面代码中都取最大值:

1
2
3
4
5
int width = 0, height = 0;
for (Camera.Size size : photoSizes) {
if (size.width > width) width = size.width;
if (size.height > height) height = size.height;
}

第四步,拍照

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
mCamera.takePicture(null, null, new android.hardware.Camera.PictureCallback() {
@Override
public void onPictureTaken(byte[] data, android.hardware.Camera camera) {//data 将会返回图片的字节数组
bitmap = BitmapFactory.decodeByteArray(data, 0, data.length);
if (bitmap != null) {
Matrix m = new Matrix();
m.postRotate(90);
bitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), m, true);
bitmap = compressImage(bitmap);
loadingTv.setVisibility(View.GONE);
cameraBtn.setVisibility(View.INVISIBLE);
cancelBtn.setVisibility(View.VISIBLE);
sureBtn.setVisibility(View.VISIBLE);
wordTv.setVisibility(View.INVISIBLE);
dateTv.setVisibility(View.INVISIBLE);
bitmap = addWater(bitmap);
pictureLinear.setVisibility(View.VISIBLE);
mSv.setVisibility(View.INVISIBLE);
pictureIv.setImageBitmap(bitmap);
} else {
releaseCamera();
}
}
});

手动调用相机拍出来的照片是旋转了 270 度的,所以要再旋转 90 度,才是正常视角m.postRotate(90)
第五步,加水印操作 addWater(bitmap):

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
android.graphics.Bitmap.Config bitmapConfig =
mBitmap.getConfig();
if (bitmapConfig == null) {
bitmapConfig = android.graphics.Bitmap.Config.ARGB_8888;
}
//获取原始图片与水印图片的宽与高
int mBitmapWidth = mBitmap.getWidth();
int mBitmapHeight = mBitmap.getHeight();

DisplayMetrics dm = getResources().getDisplayMetrics();
float screenWidth = dm.widthPixels;//1080
float mBitmapWidthF = mBitmapWidth;
times = mBitmapWidthF / screenWidth;

Bitmap mNewBitmap = Bitmap.createBitmap(mBitmapWidth, mBitmapHeight, bitmapConfig);
Canvas canvas = new Canvas(mNewBitmap);
//向位图中开始画入MBitmap原始图片
canvas.drawBitmap(mBitmap, 0, 0, null);
Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setColor(Color.WHITE);
paint.setDither(true); //获取跟清晰的图像采样
paint.setFilterBitmap(true);//过滤一些
paint.setTextSize(sp2px(this, 22) * times);
String text = "装逼水印";
Rect bounds = new Rect();
paint.getTextBounds(text, 0, text.length(), bounds);
float textW = paint.measureText(text);
float x = (mBitmapWidth / 2) - (textW / 2);
float textH = -paint.ascent() + paint.descent();
canvas.drawText(text, x, (mBitmapHeight * 3 / 4), paint);//mBitmapWidth=3024

paint.setTextSize(sp2px(this, 20) * times);
paint.getTextBounds(date, 0, date.length(), bounds);
textW = paint.measureText(date);
x = (mBitmapWidth / 2) - (textW / 2);
canvas.drawText(date, x, (mBitmapHeight * 3 / 4) + textH, paint);
canvas.save(Canvas.ALL_SAVE_FLAG);
return mNewBitmap;

说明几点:

  • 1.一开始设置字体大小是 22sp,但是没有显示水印,后来近距离仔细看有水印,只是字体太小,用了 sp 转 px,还是很小,最后发现图片的宽比手机屏宽要大得多,考虑这个倍数,计算出来,字体就可以正常显示了:times = mBitmapWidthF / screenWidth

  • 2.字体居中显示:paint.measureText(text)可以计算水印的宽度,屏宽一半减水印宽的一半,就是水印最左端的 x 坐标:
    坐标图
    高度我这边是从屏高 3/4 处开始绘制,所以最终就是居中显示在屏幕中下方:

    1
    2
    float x = (mBitmapWidth / 2) - (textW / 2);
    canvas.drawText(text, x, (mBitmapHeight * 3 / 4), paint);
  • 3.显示两行水印,并且都居中:下面水印的 y 坐标 = 上面水印 y 坐标 + 上面水印的高度,上面水印高度计算:float textH = -paint.ascent() + paint.descent()

  • 4.图片拍出来很大,压缩一下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    image.compress(Bitmap.CompressFormat.JPEG, 100, baos);// 质量压缩方法,这里100表示不压缩,把压缩后的数据存放到baos中
    int options = 98;
    while (baos.toByteArray().length / 1024 > 3072) { // 循环判断如果压缩后图片是否大于 3Mb,大于继续压缩
    baos.reset(); // 重置baos即清空baos
    image.compress(Bitmap.CompressFormat.JPEG, options, baos);// 这里压缩options%,把压缩后的数据存放到baos中
    options -= 2;// 每次都减少2
    }
    ByteArrayInputStream isBm = new ByteArrayInputStream(baos.toByteArray());// 把压缩后的数据baos存放到ByteArrayInputStream中
    Bitmap bitmap = BitmapFactory.decodeStream(isBm, null, null);// 把ByteArrayInputStream数据生成图片
    return bitmap;

第六步,保存水印图:

1
2
3
4
5
6
7
8
9
10
11
FileOutputStream outStream = null;
String filePath = Environment.getExternalStorageDirectory().getPath() + File.separator + "testPhoto";
String fileName = filePath + File.separator + String.valueOf(System.currentTimeMillis()) + ".jpg";
File file = new File(fileName);
if (!file.exists()) file.getParentFile().mkdirs();
outStream = new FileOutputStream(fileName);
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, outStream);
if (outStream != null) outStream.close();
// 最后通知图库更新
WaterCameraActivity.this.sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.parse("file://" + fileName)));
Toast.makeText(this, "文件已保存至:" + fileName, Toast.LENGTH_LONG).show();

效果图:
效果图

清晰度可以的。


附上源码点击获取
谢谢!