当前位置: 首页 > article >正文

Android如何做出带有复杂水印的图片

最近项目中存在图片加水印效果的需求,具体效果如下:

然后做出来的效果如下:

原图水印图

点击可以查看大图:大图

那么针对这种比较复杂的水印图片,应该如何去做呢?下面我分享一下自己的思路。
如果没有使用到NDK,单纯的使用Android提供的Canvas画布,那么就有一下几个步骤:

  1. 获取原始的图片地址,转化成为 sourceBitmap;
  2. 获取水印图片的Bitmap;
  3. 使用Canvas,将sourceBitmap作为底片,然后将水印Bitmap画上去;
  4. 然后将二者合并的Bitmap,保存成文件即可。

那么按照这个步骤来:

1. 原始图片赚Bitmap

这个一般很简单,用代码表示为:

Bitmap sourceBitmap = BitmapFactory.decodeFile(sourcePath);

2. 获取水印图片的Bitmap

对于复杂的水印图片,我现在的做法是,把水印图片转化成一个View,然后把View转成Bitmap。
比如上图的复杂水印图片,我们可以先画一个XML:

<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:padding="@dimen/dp_18">

    <TextView
        android:id="@+id/id_tv_time"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="40sp"
        style="@style/phone_water_mark_text_style"
        android:textStyle="bold"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <View
        android:layout_width="@dimen/dp_2"
        app:layout_constraintTop_toTopOf="@id/id_tv_time"
        app:layout_constraintBottom_toBottomOf="@id/id_tv_time"
        app:layout_constraintStart_toEndOf="@id/id_tv_time"
        android:layout_marginStart="@dimen/dp_8"
        android:background="@color/app_theme_color"
        android:id="@+id/id_center_view"
        android:layout_marginTop="@dimen/dp_10"
        android:layout_marginBottom="@dimen/dp_4"
        android:layout_height="0dp"/>

    <TextView
        android:layout_width="wrap_content"
        app:layout_constraintTop_toTopOf="@id/id_tv_time"
        app:layout_constraintStart_toEndOf="@id/id_center_view"
        android:layout_marginStart="@dimen/dp_8"
        android:textSize="14sp"
        style="@style/phone_water_mark_text_style"
        android:layout_marginTop="@dimen/dp_6"
        android:id="@+id/id_tv_date"
        android:layout_height="wrap_content"/>

    <TextView
        android:layout_width="wrap_content"
        app:layout_constraintBottom_toBottomOf="@id/id_tv_time"
        app:layout_constraintStart_toStartOf="@id/id_tv_date"
        style="@style/phone_water_mark_text_style"
        android:textSize="14sp"
        android:id="@+id/id_tv_week"
        android:layout_marginBottom="@dimen/dp_4"
        android:layout_height="wrap_content"/>

    <LinearLayout
        android:layout_width="0dp"
        android:orientation="vertical"
        app:layout_constraintTop_toBottomOf="@id/id_tv_time"
        android:layout_marginTop="@dimen/dp_8"
        app:layout_constraintStart_toStartOf="@id/id_tv_time"
        app:layout_constraintEnd_toEndOf="parent"
        android:id="@+id/id_external_info_layout"
        android:layout_height="wrap_content">

		<TextView 
		android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/id_tv_user_name"
		/>

		<TextView 
		android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/id_tv_user_location"		/>

	</LinearLayout>

</androidx.constraintlayout.widget.ConstraintLayout>

XML解析之后,大致的轮廓我们可以看到:
在这里插入图片描述
我们可以看到,XML渲染的大致轮廓就出来了,那么应该如果将View转成Bitmap呢?其实Android早就提供了类似的方法:

    fun getCurrentBitmap(): Bitmap? {
        invalidate()

        val density = resources.displayMetrics.density
        BaseLog.d("density = $density")

        val bitmapWidth = (width * density).toInt()
        val bitmapHeight = (height * density).toInt()

        val resultBitmap = Bitmap.createBitmap(bitmapWidth, bitmapHeight, Bitmap.Config.ARGB_8888)
        val canvas = Canvas(resultBitmap)
        canvas.scale(density, density)
        draw(canvas)

        return  resultBitmap
    }

代码中我新增了 density,这是为了防止不同屏幕密度下UI效果不同做的一个缩放。

3.4. Bitmap上面画水印Bitmap, 然后保存

还是先上代码吧,注释也比较清晰:

/**
     * 添加水印
     *
     * @param sourcePath 原始图片地址
     * @param watermarkBitmap 图片水印
     * @return 添加水印后的图片地址
     */
    public String addWaterMarkLayout(String sourcePath, Bitmap watermarkBitmap) {
        // 检查文件是否存在
        if (!BaseFileUtils.isFileExist(sourcePath)) {
            return sourcePath;
        }

        // 检查水印图片是否存在
        if (null == watermarkBitmap){
            BaseLog.e("watermark bitmap is null");
            return sourcePath ;
        }

        int[] bitmapWidthHeight = BaseImageUtils.getBitmapWidthHeight(sourcePath);
        if (bitmapWidthHeight[0] <= 0 || bitmapWidthHeight[1] <= 0) {
            return sourcePath;
        }

        Bitmap sourceBitmap = BitmapFactory.decodeFile(sourcePath);
        if (null == sourceBitmap){
            BaseLog.e("source bitmap is null : " + sourcePath);
            return sourcePath;
        }

		//创建一个底仓Bitmap
        Bitmap copiedBitmap = Bitmap.createBitmap(sourceBitmap.getWidth(), sourceBitmap.getHeight(), Bitmap.Config.ARGB_8888);

        Canvas targetCanvas = new Canvas(copiedBitmap);

        targetCanvas.setDrawFilter(new PaintFlagsDrawFilter(0, Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG));

        targetCanvas.drawBitmap(sourceBitmap, 0,0,null);

		// 获取watermarkBitmap 和 sourceBitmap之间的比例,好进行缩放
        int[] getSample          = checkSample(sourceBitmap, watermarkBitmap, BaseImageUtils.getOrientation(sourcePath));

        float scaleWidth =  getSample[0] * 1.0f  / watermarkBitmap.getWidth();
        float scaleHeight = getSample[1] * 1.0f / watermarkBitmap.getHeight();

        // CREATE A MATRIX FOR THE MANIPULATION
        Matrix matrix = new Matrix();
        // RESIZE THE BIT MAP
        matrix.postScale(scaleWidth, scaleHeight);
        
        //true : 以启用双线性过滤 为了就是不产生锯齿
        Bitmap scaledBitmap  = Bitmap.createBitmap(watermarkBitmap,0,0,watermarkBitmap.getWidth(),watermarkBitmap.getHeight(),matrix,true);

        Paint canvasPaint = new Paint();
        //抗锯齿
        canvasPaint.setAntiAlias(true);
        //双线性过滤
        canvasPaint.setFilterBitmap(true);

		// 画目标水印Bitmap
		// 因为是画的位置是 左下角,因此x为0,y为底座Bitmap的高度 - 缩放之后的水印Bitmap的高度 
        targetCanvas.drawBitmap(scaledBitmap, 0, copiedBitmap.getHeight() - getSample[1], canvasPaint);

		//创建一个新的临时地址,请来保存合并之后的Bitmap
        String newWaterMarkPath = BaseIOUtils.getTempImageFilePath(context);

		//保存合并之后的Bitmap到临时文件
        LzMarkBitmapUtils.saveBitmapToFile(copiedBitmap, newWaterMarkPath,85);

        bitmapWidthHeight = BaseImageUtils.getBitmapWidthHeight(newWaterMarkPath);

        if (bitmapWidthHeight[0] > 0 && bitmapWidthHeight[1] > 0) {
            //不能删除原始图片,因为Android系统会拦截
            BaseIOUtils.renameUnusedFileData(sourcePath);
            return newWaterMarkPath;
        }

        BaseLog.e("bitmapWidthHeight error");
        return sourcePath;
    }

    private int[] checkSample(Bitmap sourceBitmap, Bitmap waterMarkBitmap, int getOrientation) {
        int sourceBitmapWidth  = sourceBitmap.getWidth();
        int sourceBitmapHeight = sourceBitmap.getHeight();

        int waterMarkWidth     = waterMarkBitmap.getWidth();
        int waterMarkHeight    = waterMarkBitmap.getHeight();

        BaseLog.i("sample:" + sourceBitmapWidth + "," + sourceBitmapHeight+ "," + waterMarkWidth + "," + waterMarkHeight + "," + getOrientation);

        if (getOrientation == 90 || getOrientation == 270){ //横向
            sourceBitmapWidth = sourceBitmap.getHeight();

            if (waterMarkWidth > sourceBitmapWidth * 0.5f){
                waterMarkWidth = (int) (sourceBitmapWidth * 0.65f);
                waterMarkHeight = waterMarkBitmap.getHeight() * waterMarkWidth / waterMarkBitmap.getWidth();
            }

        } else  {

            if (waterMarkWidth > sourceBitmapWidth * 0.8f) {
                waterMarkWidth = (int) (sourceBitmapWidth * 0.8f);
                waterMarkHeight = waterMarkBitmap.getHeight() * waterMarkWidth / waterMarkBitmap.getWidth();
            }
        }


        BaseLog.i("show the water mark width :" + waterMarkWidth + "," + waterMarkHeight);

        return new int[]{(int) (waterMarkWidth * 1.3f), (int) (waterMarkHeight * 1.3f)};
    }

使用过程中出现的问题

1. 压缩问题

生成的水印图片需要上传到服务器的,为了节省带宽和流量,因此压缩是必不可少少的。我在很长一段时间内,采取的步骤为:

原始Bitmap + 水印Bitmap -> 生成带有水印的图片 -> 压缩 -> 上传

发现效果比较差,水印比较模糊,尝试了很多种方法,比如使用Paint防锯齿,先放大水印图片等等,但是效果不是很好,后来采取了另外一种思路:

原始Bitmap -> 压缩 -> 保存成文件 -> 转化成新的Bitmap + 水印Bitmap -> 合成新的Bitmap -> 保存成JPG文件 -> 上传

和上面的思路换了之后,我只合成压缩之后的Bitmap,对水印的Bitmap不进行压缩,发现效果很好。

2. 字体比较小的TextView比较模糊

对于View上的TextView如果设置字体比较小,生成的Bitmap合并之后会发现比较模糊,这可能是因为View的分辨率和最终生成的Bitmap分辨率不一致所导致的。为了解决这个问题,可以从以下几个点考虑:

  1. 调整View的分辨率
    当创建水印Bitmap时,确保View的尺寸与水印Bitmap的尺寸一致。你可以通过设置View的LayoutParams来调整View的尺寸
  1. 使用高质量的缩放方法

在使用Bitmap.createBitmap()时,确保使用高质量的缩放方法来生成带有水印的图片。使用Bitmap.createScaledBitmap()方法可以实现这一目的。

Bitmap originalBitmap = ...; // 原始图片
Bitmap watermarkBitmap = ...; // 水印图片
int watermarkWidth = watermarkBitmap.getWidth();
int watermarkHeight = watermarkBitmap.getHeight();

// 缩放水印图片
int newWatermarkWidth = ...; // 新的水印宽度
int newWatermarkHeight = ...; // 新的水印高度
Bitmap scaledWatermarkBitmap = Bitmap.createScaledBitmap(watermarkBitmap, newWatermarkWidth, newWatermarkHeight, true);

  1. 使用适当的质量参数进行Bitmap保存

在将Bitmap保存为JPEG或PNG格式的图片时,确保使用适当的质量参数。一般情况下,JPEG的质量参数范围是0-100,PNG是无损压缩,质量参数对其没有影响。

Bitmap finalBitmap = ...; // 最终生成的带有水印的图片
FileOutputStream outputStream = ...; // 输出流
int quality = 100; // 质量参数

finalBitmap.compress(Bitmap.CompressFormat.JPEG, quality, outputStream);

基本上一个稍微负责的水印图片,我们基本上就成功了,如果有什么疑问或者有什么错误,请及时指出,或者可以联系我wx:javainstalling,备注:水印 即可。


http://www.kler.cn/a/7881.html

相关文章:

  • 【论文阅读】WaDec: Decompiling WebAssembly Using Large Language Model
  • DBeaver 连接 OceanBase Oracle 租户
  • websocket初始化
  • Spring Cloud Gateway(分发请求)
  • Java 责任链模式 减少 if else 实战案例
  • 执行flink sql连接clickhouse库
  • Web基础与HTTP协议
  • Maven项目中的依赖出现版本冲突,最终发现是对Dependency Scope理解有误
  • Win11的两个实用技巧系列之找不到wifi网络的解决方法、双系统开机选择系统方法
  • 数据库系统工程师——考试分析(2023备考)
  • 【虚幻引擎】UE4 动画蓝图,动画,状态机三者之间的联系
  • UTONMOS:2023年,亚洲或将实现区块链游戏复兴
  • 了解Mysql
  • 【区块链技术开发】基于Web3.js以太坊网络上的智能合约的交互及其应用
  • GP03丨宽窄基资金管理增强策略
  • PyQt5可视化 7 饼图和柱状图实操案例 ③柱状图的实现【超详解】
  • java-replace into详解(SQL)
  • 面试Interview
  • ChatGPT指令大全(中文版)
  • 【C++】模板进阶(非类型模板参数、类模板的特化和模板的分离编译)
  • asa(苹果Apple Search Ads平台)授权调用接口
  • 【设置应用程序图标-启动图片 Objective-C语言】
  • Vue:初识Vue
  • 数字化可视化大屏:创新与融合的未来
  • 02.数据结构之算法
  • 指针的详细运用介绍(高阶)