一篇文章实现Android图片拼接并保存至相册

系列文章目录

一篇文章实现Android图片拼接并保存至相册



前言

好久没有写Android系列的文章了,最近有小伙伴问到了Android图片拼接的问题,写一篇相关的博客。

在Android应用中实现图片拼接功能并保存到相册是一个常见的需求,比如制作全景图、拼图应用或照片编辑工具。本文将介绍如何实现一个完整的图片拼接应用,包括图片选择、拼接和保存功能。


实现功能

  1. 检查并请求必要的存储权限
  2. 允许用户从相册选择一张或多张图片
  3. 异步加载选中的图片
  4. 使用ImageStitcher类拼接图片
  5. 将拼接后的图片保存到相册
  6. 在整个过程中显示适当的进度指示和操作反馈

类定义和成员变量

其中包括图片选择请求码,读取权限请求码, 写入权限请求码,保存目录名称,以及相关控件。

java 复制代码
public class MainActivity extends AppCompatActivity {
    private static final int PICK_IMAGE_REQUEST = 1;  // 图片选择请求码
    private static final int REQUEST_PERMISSION = 2;  // 读取权限请求码
    private static final int REQUEST_WRITE_PERMISSION = 3;  // 写入权限请求码
    private static final String SAVE_DIRECTORY = "ImageStitcher";  // 保存目录名称
    
    private List<Bitmap> selectedImages = new ArrayList<>();  // 存储选择的图片
    private ImageView resultView;  // 显示拼接结果的ImageView
    private ProgressBar progressBar;  // 进度条
    private Button selectBtn, stitchBtn, saveBtn;  // 按钮控件

onCreate方法

初始化控件以及设置监听

java 复制代码
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);  // 设置布局文件

    // 初始化视图控件
    resultView = findViewById(R.id.jm_result_image);
    progressBar = findViewById(R.id.jm_progress_bar);
    selectBtn = findViewById(R.id.jm_select_btn);
    stitchBtn = findViewById(R.id.jm_stitch_btn);
    saveBtn = findViewById(R.id.jm_save_btn);
    saveBtn.setVisibility(View.GONE);  // 初始时隐藏保存按钮

    // 设置按钮点击监听器
    selectBtn.setOnClickListener(v -> checkPermissionAndOpenChooser());
    stitchBtn.setOnClickListener(v -> stitchImagesAsync());
}

权限检查和图片选择

不动态申请权限小心报错:has no access to content 需在AndroidManifest.xml声明READ_EXTERNAL_STORAGE权限,Android Q及以上版本必须使用MediaStore API访问公共目录文件。

java 复制代码
private void checkPermissionAndOpenChooser() {
    // 检查是否有读取外部存储权限
    if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE)
            == PackageManager.PERMISSION_GRANTED) {
        openImageChooser();  // 有权限则直接打开图片选择器
    } else {
        // 没有权限则请求权限
        ActivityCompat.requestPermissions(this,
                new String[]{Manifest.permission.READ_EXTERNAL_STORAGE},
                REQUEST_PERMISSION);
    }
}

private void openImageChooser() {
    // 创建选择图片的Intent
    Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
    intent.setType("image/*");  // 设置类型为图片
    intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);  // 允许多选
    startActivityForResult(Intent.createChooser(intent, "选择图片"), PICK_IMAGE_REQUEST);
}

// 权限请求结果回调
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
    super.onRequestPermissionsResult(requestCode, permissions, grantResults);
    if (requestCode == REQUEST_PERMISSION && grantResults.length > 0
            && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
        openImageChooser();  // 权限被授予后打开图片选择器
    }
}

处理选择的图片

java 复制代码
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    super.onActivityResult(requestCode, resultCode, data);
    if (requestCode == PICK_IMAGE_REQUEST && resultCode == RESULT_OK && data != null) {
        handleSelectedImages(data);  // 处理选择的图片
    }
}

private void handleSelectedImages(Intent data) {
    progressBar.setVisibility(View.VISIBLE);  // 显示进度条
    ExecutorService executor = Executors.newSingleThreadExecutor();
    executor.execute(() -> {
        try {
            if (data.getClipData() != null) {
                processMultipleImages(data.getClipData());  // 处理多张图片
            } else if (data.getData() != null) {
                processSingleImage(data.getData());  // 处理单张图片
            }
        } finally {
            runOnUiThread(() -> progressBar.setVisibility(View.GONE));  // 隐藏进度条
        }
    });
}

private void processMultipleImages(ClipData clipData) {
    for (int i = 0; i < clipData.getItemCount(); i++) {
        loadAndAddImage(clipData.getItemAt(i).getUri());  // 加载并添加每张图片
    }
}

private void processSingleImage(Uri uri) {
    loadAndAddImage(uri);  // 加载并添加单张图片
}

private void loadAndAddImage(Uri uri) {
    try (InputStream is = getContentResolver().openInputStream(uri)) {
        Bitmap bitmap = BitmapFactory.decodeStream(is);  // 从URI加载图片
        runOnUiThread(() -> {
            selectedImages.add(bitmap);  // 添加到图片列表
            Toast.makeText(this, "成功加载图片", Toast.LENGTH_SHORT).show();
        });
    } catch (Exception e) {
        runOnUiThread(() ->
                Toast.makeText(this, "加载失败: " + e.getMessage(), Toast.LENGTH_SHORT).show());
    }
}

图片拼接功能

java 复制代码
private void stitchImagesAsync() {
    if (selectedImages.isEmpty()) return;  // 如果没有选择图片则返回
    
    saveBtn.setVisibility(View.VISIBLE);  // 显示保存按钮
    progressBar.setVisibility(View.VISIBLE);  // 显示进度条
    
    ExecutorService executor = Executors.newSingleThreadExecutor();
    executor.execute(() -> {
        // 调用ImageStitcher类拼接图片
        Bitmap stitched = ImageStitcher.stitchImages(
                selectedImages.toArray(new Bitmap[0]), 0);
        
        runOnUiThread(() -> {
            resultView.setImageBitmap(stitched);  // 显示拼接结果
            progressBar.setVisibility(View.GONE);  // 隐藏进度条
            saveBtn.setVisibility(View.VISIBLE);  // 确保保存按钮可见
            // 设置保存按钮点击监听器
            saveBtn.setOnClickListener(v -> saveImageToGallery(stitched));
        });
    });
}

图片保存功能

java 复制代码
private void saveImageToGallery(Bitmap bitmap) {
    // 检查是否有写入外部存储权限
    if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)
            != PackageManager.PERMISSION_GRANTED) {
        // 没有权限则请求权限
        ActivityCompat.requestPermissions(this,
                new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},
                REQUEST_WRITE_PERMISSION);
        return;
    }

    // 在新线程中执行保存操作
    new Thread(() -> {
        String fileName = "stitched_" + System.currentTimeMillis() + ".jpg";
        ContentValues values = new ContentValues();
        values.put(MediaStore.Images.Media.DISPLAY_NAME, fileName);
        values.put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg");

        // 对于Android Q及以上版本
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            values.put(MediaStore.Images.Media.RELATIVE_PATH, 
                     Environment.DIRECTORY_PICTURES + File.separator + SAVE_DIRECTORY);
            values.put(MediaStore.Images.Media.IS_PENDING, 1);
        }

        try {
            // 插入媒体库记录
            Uri uri = getContentResolver().insert(
                    MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);

            try (OutputStream os = getContentResolver().openOutputStream(uri)) {
                // 压缩并写入图片数据
                bitmap.compress(Bitmap.CompressFormat.JPEG, 90, os);
                
                // 对于Android Q及以上版本,更新IS_PENDING标志
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
                    values.put(MediaStore.Images.Media.IS_PENDING, 0);
                    getContentResolver().update(uri, values, null, null);
                }
                
                // 显示保存成功提示
                runOnUiThread(() ->
                        Toast.makeText(this, "图片已保存至相册", Toast.LENGTH_SHORT).show());
            }
        } catch (Exception e) {
            // 显示保存失败提示
            runOnUiThread(() ->
                    Toast.makeText(this, "保存失败: " + e.getMessage(), Toast.LENGTH_SHORT).show());
        }
    }).start();
}

使用ImageStitcher类拼接图片

代码解释:ImageStitcher.java

这是一个用于拼接多张图片的工具类,提供了将多张图片横向或纵向拼接成一张大图的功能。下面是对代码的详细解释:

类定义和方法

java 复制代码
public class ImageStitcher {
    public static Bitmap stitchImages(Bitmap[] images, int direction) {
        // 检查输入参数是否有效
        if (images == null || images.length == 0) return null;

计算拼接后图片的尺寸

java 复制代码
        int width = images[0].getWidth();
        int height = images[0].getHeight();

        // 计算拼接后图片的总尺寸
        if (direction == 0) { // 横向拼接
            for (int i = 1; i < images.length; i++) {
                width += images[i].getWidth();  // 累加宽度
                height = Math.max(height, images[i].getHeight());  // 取最大高度
            }
        } else { // 纵向拼接
            for (int i = 1; i < images.length; i++) {
                height += images[i].getHeight();  // 累加高度
                width = Math.max(width, images[i].getWidth());  // 取最大宽度
            }
        }

计算逻辑

  • 横向拼接:总宽度为所有图片宽度之和,高度为所有图片中的最大高度
  • 纵向拼接:总高度为所有图片高度之和,宽度为所有图片中的最大宽度

创建并绘制拼接后的图片

java 复制代码
        // 创建拼接后的Bitmap
        Bitmap result = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
        Canvas canvas = new Canvas(result);

        // 绘制图片
        int currentPos = 0;
        for (Bitmap image : images) {
            if (direction == 0) { // 横向拼接
                canvas.drawBitmap(image, currentPos, 0, null);  // 在当前位置绘制图片
                currentPos += image.getWidth();  // 更新横向位置
            } else { // 纵向拼接
                canvas.drawBitmap(image, 0, currentPos, null);  // 在当前位置绘制图片
                currentPos += image.getHeight();  // 更新纵向位置
            }
        }

        return result;  // 返回拼接后的图片
    }
}

绘制过程

  1. 创建一个新的空白Bitmap,大小为之前计算的总尺寸
  2. 使用Canvas在这个Bitmap上绘制所有输入图片
  3. 根据拼接方向,依次将每张图片绘制到正确的位置
  4. 更新当前位置指针(currentPos),以便下一张图片绘制在正确的位置

注意事项

  • 所有输入图片应为非空且尺寸相同(代码中未做严格检查)
  • 拼接方向通过简单的int值判断(0为横向,非0为纵向)
  • 使用了ARGB_8888配置创建Bitmap,保证图片质量
  • 这是一个基础实现,没有处理图片尺寸不一致时的缩放或裁剪

效果图

源码

MainActivity.java

java 复制代码
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;

import android.Manifest;
import android.content.ClipData;
import android.content.ContentValues;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.provider.MediaStore;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.ProgressBar;
import android.widget.Toast;

import java.io.File;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;


public class MainActivity extends AppCompatActivity {
    private static final int PICK_IMAGE_REQUEST = 1;
    private static final int REQUEST_PERMISSION = 2;
    private List<Bitmap> selectedImages = new ArrayList<>();
    private ImageView resultView;
    private ProgressBar progressBar;
    private static final int REQUEST_WRITE_PERMISSION = 3;
    private static final String SAVE_DIRECTORY = "JmImgStitcher";
    private Button selectBtn,stitchBtn,saveBtn;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        resultView = findViewById(R.id.jm_result_image);
        progressBar = findViewById(R.id.jm_progress_bar);
        selectBtn = findViewById(R.id.jm_select_btn);
        stitchBtn = findViewById(R.id.jm_stitch_btn);
        // 初始化保存按钮
        saveBtn = findViewById(R.id.jm_save_btn);
        saveBtn.setVisibility(View.GONE);

        selectBtn.setOnClickListener(v -> checkPermissionAndOpenChooser());
        stitchBtn.setOnClickListener(v -> stitchImagesAsync());
    }

    private void checkPermissionAndOpenChooser() {
        if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE)
                == PackageManager.PERMISSION_GRANTED) {
            openImageChooser();
        } else {
            ActivityCompat.requestPermissions(this,
                    new String[]{Manifest.permission.READ_EXTERNAL_STORAGE},
                    REQUEST_PERMISSION);
        }
    }

    private void openImageChooser() {
        Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
        intent.setType("image/*");
        intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
        startActivityForResult(Intent.createChooser(intent, "选择图片"), PICK_IMAGE_REQUEST);
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        if (requestCode == REQUEST_PERMISSION && grantResults.length > 0
                && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
            openImageChooser();
        }
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if (requestCode == PICK_IMAGE_REQUEST && resultCode == RESULT_OK && data != null) {
            handleSelectedImages(data);
        }
    }

    private void handleSelectedImages(Intent data) {
        progressBar.setVisibility(View.VISIBLE);
        ExecutorService executor = Executors.newSingleThreadExecutor();
        executor.execute(() -> {
            try {
                if (data.getClipData() != null) {
                    processMultipleImages(data.getClipData());
                } else if (data.getData() != null) {
                    processSingleImage(data.getData());
                }
            } finally {
                runOnUiThread(() -> progressBar.setVisibility(View.GONE));
            }
        });
    }

    private void processMultipleImages(ClipData clipData) {
        for (int i = 0; i < clipData.getItemCount(); i++) {
            loadAndAddImage(clipData.getItemAt(i).getUri());
        }
    }

    private void processSingleImage(Uri uri) {
        loadAndAddImage(uri);
    }

    private void loadAndAddImage(Uri uri) {
        try (InputStream is = getContentResolver().openInputStream(uri)) {
            Bitmap bitmap = BitmapFactory.decodeStream(is);
            runOnUiThread(() -> {
                selectedImages.add(bitmap);
                Toast.makeText(this, "成功加载图片", Toast.LENGTH_SHORT).show();
            });
        } catch (Exception e) {
            runOnUiThread(() ->
                    Toast.makeText(this, "加载失败: " + e.getMessage(), Toast.LENGTH_SHORT).show());
        }
    }

    // 修改stitchImagesAsync方法
    private void stitchImagesAsync() {
        if (selectedImages.isEmpty()) return;
        saveBtn.setVisibility(View.VISIBLE);
        progressBar.setVisibility(View.VISIBLE);
        ExecutorService executor = Executors.newSingleThreadExecutor();
        executor.execute(() -> {
            Bitmap stitched = ImageStitcher.stitchImages(
                    selectedImages.toArray(new Bitmap[0]), 0);
            runOnUiThread(() -> {
                resultView.setImageBitmap(stitched);
                progressBar.setVisibility(View.GONE);
                //设置出现
                saveBtn.setVisibility(View.VISIBLE);
                saveBtn.setOnClickListener(v -> saveImageToGallery(stitched));
            });
        });

    }

    private void saveImageToGallery(Bitmap bitmap) {
        if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)
                != PackageManager.PERMISSION_GRANTED) {
            ActivityCompat.requestPermissions(this,
                    new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},
                    REQUEST_WRITE_PERMISSION);
            return;
        }

        new Thread(() -> {
            String fileName = "stitched_" + System.currentTimeMillis() + ".jpg";
            ContentValues values = new ContentValues();
            values.put(MediaStore.Images.Media.DISPLAY_NAME, fileName);
            values.put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg");

            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
                values.put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_PICTURES + File.separator + SAVE_DIRECTORY);
                values.put(MediaStore.Images.Media.IS_PENDING, 1);
            }

            try {
                Uri uri = getContentResolver().insert(
                        MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);

                try (OutputStream os = getContentResolver().openOutputStream(uri)) {
                    bitmap.compress(Bitmap.CompressFormat.JPEG, 90, os);
                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
                        values.put(MediaStore.Images.Media.IS_PENDING, 0);
                        getContentResolver().update(uri, values, null, null);
                    }
                    runOnUiThread(() ->
                            Toast.makeText(this, "图片已保存至相册", Toast.LENGTH_SHORT).show());
                }
            } catch (Exception e) {
                runOnUiThread(() ->
                        Toast.makeText(this, "保存失败: " + e.getMessage(), Toast.LENGTH_SHORT).show());
            }
        }).start();
    }

}

ImageStitcher.java

java 复制代码
import android.graphics.Bitmap;
import android.graphics.Canvas;

public class ImageStitcher {
    public static Bitmap stitchImages(Bitmap[] images, int direction) {
        if (images == null || images.length == 0) return null;

        int width = images[0].getWidth();
        int height = images[0].getHeight();

        // 计算拼接后图片的总尺寸
        if (direction == 0) { // 横向拼接
            for (int i = 1; i < images.length; i++) {
                width += images[i].getWidth();
                height = Math.max(height, images[i].getHeight());
            }
        } else { // 纵向拼接
            for (int i = 1; i < images.length; i++) {
                height += images[i].getHeight();
                width = Math.max(width, images[i].getWidth());
            }
        }

        // 创建拼接后的Bitmap
        Bitmap result = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
        Canvas canvas = new Canvas(result);

        // 绘制图片
        int currentPos = 0;
        for (Bitmap image : images) {
            if (direction == 0) { // 横向拼接
                canvas.drawBitmap(image, currentPos, 0, null);
                currentPos += image.getWidth();
            } else { // 纵向拼接
                canvas.drawBitmap(image, 0, currentPos, null);
                currentPos += image.getHeight();
            }
        }

        return result;
    }
}

AndroidManifest权限申明

java 复制代码
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <!-- Android 10+ 需要添加 -->
    <uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION"
        android:maxSdkVersion="29" />

activity_main.xml

java 复制代码
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">
    <ProgressBar
        android:id="@+id/jm_progress_bar"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:visibility="gone"/>
    <Button
        android:id="@+id/jm_select_btn"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="选择要拼接的图片"/>

    <Button
        android:id="@+id/jm_stitch_btn"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="开始拼接图片"/>
    <Button
        android:id="@+id/jm_save_btn"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="保存图片"
        android:visibility="gone"/>
    <ImageView
        android:id="@+id/jm_result_image"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:scaleType="centerInside"/>
</LinearLayout>

总结

此文章可以作为基础,根据具体需求进行扩展和优化。欢迎留言,如有问题可以联系计蒙。

相关推荐
FmZero1 分钟前
后端全栈路线(9小时前端速成)
前端·vscode·学习
万世浮华戏骨3 分钟前
Web 后端 Python 基础安全
前端·python·安全
Dontla5 分钟前
JWT认证流程(JSON Web Token)
前端·数据库·json
無限進步D5 小时前
Java 运行原理
java·开发语言·入门
難釋懷5 小时前
安装Canal
java
是苏浙5 小时前
JDK17新增特性
java·开发语言
余人于RenYu6 小时前
Claude + Figma MCP
前端·ui·ai·figma
阿里加多8 小时前
第 4 章:Go 线程模型——GMP 深度解析
java·开发语言·后端·golang
杨艺韬8 小时前
vite内核解析-第2章 架构总览
前端·vite
likerhood9 小时前
java中`==`和`.equals()`区别
java·开发语言·python