Android调用相机拍照并添加水印

最近项目提出新需求,要求在拍照后在图片上添加用户信息的水印,so,楼主重新整理封装了一个简单的拍照+水印的工具类,需求不同,仅供参考。文件操作及权限做了适配,支持androidN及以上版本,放心使用。

效果演示:



如何调用相机这里就不介绍了,因为小伙伴们基本都使用过,主要介绍下实现水印的方式,实现水印效果也很简单,利用paint及canvas操作bitmap在对应位置进行绘制文本即可,首先定义画笔,设置水印颜色、大小及文本:

Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setColor(color);
paint.setTextSize(size);
Rect bounds = new Rect();
paint.getTextBounds(text, 0, text.length(), bounds);

定义好画笔后新建canvas对bitmap进行绘制,这里应该注意的是在Android代码里是不允许直接修改Bitmap资源文件,如果不copy一份的话会抛异常

Caused by: java.lang.IllegalStateException: Immutable bitmap passed to Canvas constructor

绘制代码如下:

Bitmap.Config bitmapConfig = bitmap.getConfig();
paint.setDither(true); // 获取跟清晰的图像采样
paint.setFilterBitmap(true);// 过滤一些
if (bitmapConfig == null) {
    bitmapConfig = Bitmap.Config.ARGB_8888;
}
bitmap = bitmap.copy(bitmapConfig, true);
Canvas canvas = new Canvas(bitmap);
canvas.drawText(text, paddingLeft, paddingTop, paint);

我们看下drawText的源码

/**
 * Draw the text, with origin at (x,y), using the specified paint. The
 * origin is interpreted based on the Align setting in the paint.
 *
 * @param text  The text to be drawn
 * @param x     The x-coordinate of the origin of the text being drawn
 * @param y     The y-coordinate of the baseline of the text being drawn
 * @param paint The paint used for the text (e.g. color, size, style)
 */
public void drawText(@NonNull String text, float x, float y, @NonNull Paint paint) {
    native_drawText(mNativeCanvasWrapper, text, 0, text.length(), x, y, paint.mBidiFlags,
            paint.getNativeInstance(), paint.mNativeTypeface);
}

第一个和第四个参数显而易见,第二个和第三个看注释也能看懂,当然是对应的坐标轴x轴和y轴了,但是这个坐标轴到底原点在哪呢,楼主自己做了一张简单的图片供小伙伴参考,如图:


坐标原点为手机左上角,横向向右为X轴正极,纵向向下为Y轴正极,这样我们可以计算left及top来设置水印的不同位置了,楼主在util中封装了几个常用的位置,小伙伴可以查看demo中的代码。

绘制文本问题解决了,但是又一个新问题出现了,那就是如何实现多段落显示及段落间的换行,我们都知道每部手机的相机像素是不一样的,如果是把textSize写死的话,在不同分辨率的图片上显示效果很差,所以我定义了一个itemCount,每行显示的文字数,通过itemCount及图片宽度计算出textSize,比如一部手机拍出的图片宽为1080像素,itemCount为20,那么文字大小为54px,根据textSize计算总文本行数并分段,把每段文字放在list中,最后遍历List绘制水印,参考代码如下:

/**
 * 绘制水印
 *
 * @param context
 * @param bitmap
 * @param waterMaskParam
 * @return
 */
private static Bitmap drawTxt(Context context, Bitmap bitmap, WaterMaskParam waterMaskParam) {
    int maxHeight = 0;  //计算总行数
    List<List<String>> msg = new ArrayList<>(); //文本
    for (String str : waterMaskParam.txt) {
        int count = str.length() / waterMaskParam.itemCount;
        if (count == 0) {
            maxHeight++;
            List<String> list = new ArrayList<>();
            list.add(str);
            msg.add(list);
        } else {
            if (str.length() % waterMaskParam.itemCount != 0) {
                count++;
            }
            List<String> list = new ArrayList<>();
            for (int i = 0; i < count; i++) {
                String s = str.substring(i * waterMaskParam.itemCount,
                        i == count - 1 ? str.length() : (i + 1) * waterMaskParam.itemCount);
                list.add(s);
            }
            msg.add(list);
            maxHeight += count;
        }
    }
    int txtSize = bitmap.getWidth() / waterMaskParam.itemCount;
    int index = msg.size() - 1;
    bitmap = checkBackground(waterMaskParam.location, bitmap, msg.size() * txtSize * 2.0f);
    for (List<String> strings : msg) {
        for (int i = 0; i < strings.size(); i++) {
            bitmap = checkType(context, bitmap, strings.get(i), txtSize,
                    waterMaskParam, txtSize * (maxHeight--) + index * txtSize / 2);
        }
        index--;
    }
    return bitmap;
}
我在最后绘制水印时,list中嵌套了一个list,外循环用来处理多段落的换行问题,内循环用来处理每个段落中的换行问题,所以我在调用绘制文本外部类传值时用的是list而不是string,及list中每个元素为一个段落,model如下:

public static class WaterMaskParam {
    public List<String> txt = new ArrayList<>(); //水印文字
    public int itemCount = DefWaterMaskParam.ITEM_COUNT; //每行的文字数
    public int txtColor = DefWaterMaskParam.TEXT_COLOR; //文字颜色
    public int location = DefWaterMaskParam.Location.left_bottom; //水印位置

    public WaterMaskParam() {
    }

    public WaterMaskParam(List<String> txt) {
        this.txt = txt;
    }

    public WaterMaskParam(List<String> txt, int itemCount, int txtColor) {
        this.txt = txt;
        this.itemCount = itemCount;
        this.txtColor = txtColor;
    }
}
为了优化水印,我在水印背景处又加了印象效果,模拟器拍摄的图片,不美观见谅


背景色的实现方式与水印绘制方式大同小异:

/**
 * 绘制背景色
 *
 * @param bitmap
 * @param maxHeight 水印最大高度
 * @return
 */
private static Bitmap drawBackground(Bitmap bitmap, float maxHeight) {
    Bitmap newBitmap = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(),
            Bitmap.Config.ARGB_8888);
    Canvas canvas = new Canvas(newBitmap);
    Paint paint_b = new Paint();
    paint_b.setDither(true);
    paint_b.setFilterBitmap(true);
    paint_b.setColor(Color.BLACK);
    paint_b.setDither(true);
    paint_b.setFilterBitmap(true);
    paint_b.setAlpha(100);
    canvas.drawBitmap(bitmap, 0, 0, null);
    canvas.drawRect(0, newBitmap.getHeight() - maxHeight, newBitmap.getWidth(),
            newBitmap.getHeight(), paint_b);
    return newBitmap;
}

我封装了一个helper类用来简化调用:

public class WaterMaskHelper {

    private Context context;

    private PhotoListener photoListener;
    private WaterMask.WaterMaskListener waterMarkListener;

    public WaterMaskHelper(Context context, PhotoListener photoListener, WaterMask.WaterMaskListener waterMarkListener) {
        this.context = context;
        this.photoListener = photoListener;
        this.waterMarkListener = waterMarkListener;
    }

    public void startCapture() {
        context.startActivity(new Intent(context, PhotoCaptureActivity.class));
        PhotoCaptureActivity.setWaterListener(waterMarkListener);
        PhotoCaptureActivity.setPhotoListener(photoListener);
    }
}

在activity中实例helper并实现接口:

//初始化水印工具
waterMaskHelper = new WaterMaskHelper(this, this, this);

调用startCapture调用相机拍照并添加水印:

waterMaskHelper.startCapture();

暴露的接口有两个,都是在相机拍照后调用,选择照片:

//选择照片的uri,默认为下标1的元素
void onChoose(ArrayList<String> photos);
添加水印:

//拍照后调用,设置水印的基本参数
WaterMaskParam onDraw();

具体实现,注:txt为空或param为空时不绘制水印

@Override
public WaterMask.WaterMaskParam onDraw() {
    WaterMask.WaterMaskParam param = new WaterMask.WaterMaskParam();
    param.txt.add("我是一个小标题");
    param.txt.add(binding.edt.getText().toString().trim());
    param.location = maskLocation;
    param.itemCount = 30;
    return param;
}

@Override
public void onChoose(ArrayList<String> photos) {
    uris = photos;
    Glide.with(MainActivity.this).load(photos.get(0)).placeholder(R.mipmap.ic_launcher).centerCrop().error(R.mipmap.ic_launcher)
            .crossFade().into(binding.img);
}
相关Demo已上传至Github, Git地址

Demo中只支持文本绘制,添加图片的实现方式其实与文字差不多,有兴趣的小伙伴可以尝试。我结合了相机及水印,想单独调用的是可以代码分离的哦,耦合度较低。

您可以使用 Android 相机 API 来实现拍照添加地址水印。以下是一个简单的示例代码,展示了如何实现这个功能: ```java import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.location.Address; import android.location.Geocoder; import android.location.Location; import android.media.ExifInterface; import android.media.Image; import android.media.ImageReader; import android.os.Bundle; import android.os.Environment; import android.util.Log; import androidx.annotation.NonNull; import androidx.appcompat.app.AppCompatActivity; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.nio.ByteBuffer; import java.text.SimpleDateFormat; import java.util.Date; import java.util.List; import java.util.Locale; public class MainActivity extends AppCompatActivity { private static final String TAG = "MainActivity"; private static final int IMAGE_WIDTH = 1920; // 图片宽度 private static final int IMAGE_HEIGHT = 1080; // 图片高度 private ImageReader.OnImageAvailableListener mOnImageAvailableListener = new ImageReader.OnImageAvailableListener() { @Override public void onImageAvailable(ImageReader reader) { Image image = reader.acquireLatestImage(); ByteBuffer buffer = image.getPlanes()[0].getBuffer(); byte[] bytes = new byte[buffer.remaining()]; buffer.get(bytes); // 将图片保存到文件 File file = saveImage(bytes); if (file != null) { try { // 读取图片的经纬度信息 ExifInterface exifInterface = new ExifInterface(file.getAbsolutePath()); float[] latLong = new float[2]; if (exifInterface.getLatLong(latLong)) { // 将经纬度转换为地址 String address = getAddressFromLocation(latLong[0], latLong[1]); // 在图片上绘制地址水印 Bitmap bitmap = drawTextOnBitmap(bytes, address); // 保存加上水印的图片 saveImage(bitmap); } } catch (IOException e) { e.printStackTrace(); } } image.close(); } }; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // 创建一个 ImageReader,监听相机拍照的图像数据 ImageReader imageReader = ImageReader.newInstance(IMAGE_WIDTH, IMAGE_HEIGHT, ImageFormat.JPEG, 1); imageReader.setOnImageAvailableListener(mOnImageAvailableListener, null); // 打开相机进行拍照 CameraManager cameraManager = (CameraManager) getSystemService(Context.CAMERA_SERVICE); try { String cameraId = cameraManager.getCameraIdList()[0]; cameraManager.openCamera(cameraId, new CameraDevice.StateCallback() { @Override public void onOpened(@NonNull CameraDevice camera) { try { CaptureRequest.Builder captureRequestBuilder = camera.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE); captureRequestBuilder.addTarget(imageReader.getSurface()); captureRequestBuilder.set(CaptureRequest.CONTROL_MODE, CameraMetadata.CONTROL_MODE_AUTO); camera.createCaptureSession(Collections.singletonList(imageReader.getSurface()), new CameraCaptureSession.StateCallback() { @Override public void onConfigured(@NonNull CameraCaptureSession session) { try { session.capture(captureRequestBuilder.build(), null, null); } catch (CameraAccessException e) { e.printStackTrace(); } } @Override public void onConfigureFailed(@NonNull CameraCaptureSession session) { Log.e(TAG, "Failed to configure camera capture session"); } }, null); } catch (CameraAccessException e) { e.printStackTrace(); } } @Override public void onDisconnected(@NonNull CameraDevice camera) { camera.close(); } @Override public void onError(@NonNull CameraDevice camera, int error) { camera.close(); } }, null); } catch (CameraAccessException e) { e.printStackTrace(); } } private File saveImage(byte[] bytes) { File file = null; try { String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(new Date()); File storageDir = getExternalFilesDir(Environment.DIRECTORY_PICTURES); file = new File(storageDir, "IMG_" + timeStamp + ".jpg"); FileOutputStream output = new FileOutputStream(file); output.write(bytes); output.close(); } catch (IOException e) { e.printStackTrace(); } return file; } private String getAddressFromLocation(double latitude, double longitude) { Geocoder geocoder = new Geocoder(this, Locale.getDefault()); String addressStr = ""; try { List<Address> addresses = geocoder.getFromLocation(latitude, longitude, 1); if (addresses != null && addresses.size() > 0) { Address address = addresses.get(0); addressStr = address.getAddressLine(0); } } catch (IOException e) { e.printStackTrace(); } return addressStr; } private Bitmap drawTextOnBitmap(byte[] bytes, String text) { Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length).copy(Bitmap.Config.ARGB_8888, true); Canvas canvas = new Canvas(bitmap); Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); paint.setColor(Color.WHITE); paint.setTextSize(50); float x = 50; float y = 50; canvas.drawText(text, x, y, paint); return bitmap; } private void saveImage(Bitmap bitmap) { File file = null; try { String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(new Date()); File storageDir = getExternalFilesDir(Environment.DIRECTORY_PICTURES); file = new File(storageDir, "IMG_" + timeStamp + ".jpg"); FileOutputStream output = new FileOutputStream(file); bitmap.compress(Bitmap.CompressFormat.JPEG, 100, output); output.close(); } catch (IOException e) { e.printStackTrace(); } if (file != null) { // 在这里可以将图片路径传递给其他组件或进行其他操作 Log.d(TAG, "保存图片成功: " + file.getAbsolutePath()); } } } ``` 这个示例代码使用了相机 API 进行拍照,并通过 ImageReader 获取图像数据。然后,它通过 ExifInterface 读取图片的经纬度信息,并使用 Geocoder 将经纬度转换为地址。最后,它在图片上绘制地址水印,并保存加上水印的图片。 请注意,这只是一个简单的示例,您可能需要根据自己的需求进行修改和优化。同时,为了使用位置信息,您需要在应用的 AndroidManifest.xml 文件中添加相应的权限声明和特殊权限请求。 希望这可以帮助到您!如果有任何问题,请随时提问。
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值