开发者

Android实现图片拼接并保存至相册

开发者 https://www.devze.com 2025-06-11 10:18 出处:网络 作者: 计蒙不吃鱼
目录前言实现功能类定义和成员变量onCreate方法权限检查和图片选择处理选择的图片图片拼接功能图片保存功能使用ImageStitcher类拼接图片类定义和方法计算拼接后图片的尺寸计算逻辑创建并绘制拼接后的图片绘制过程注意
目录
  • 前言
  • 实现功能
  • 类定义和成员变量
    • onCreate方法
    • 权限检查和图片选择
    • 处理选择的图片
    • 图片拼接功能
    • 图片保存功能
  • 使用ImageStitcher类拼接图片
    • 类定义和方法
    • 计算拼接后图片的尺寸
    • 计算逻辑
    • 创建并绘制拼接后的图片
    • 绘制过程
    • 注意事项
  • 效果图
    • 源码
      • MainActivity.Java
      • ImageStitcher.java
      • androidManifest权限申明
      • activity_main.XML

    前言

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

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

    实现功能

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

    类定义和成员变量

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

    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方法

    初始化控件以及设置监听

    @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访问公共目录文件。

    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();  // 权限被授予后打开图片选择器
        }
    }
    

    处理选择的图片

    @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 handleSelectedImagandroides(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);  // 加载并添加单张图片
    }
    
    privjsate 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());
        }
    }
    

    图片拼接功能

    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));
            });
        });
    }
    

    图片保存功能

    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

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

    类定义和方法

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

    js算拼接后图片的尺寸

            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 { /http://www.devze.com/ 纵向拼接
                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;  // 返回拼接后的图片
        }
    }
    

    绘制过程

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

    注意事项

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

    效果图

    Android实现图片拼接并保存至相册

    源码

    MainActivity.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

    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权限申明

        <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

    <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>

    以上就是Android实现图片拼接并保存至相册的详细内容,更多关于Android图片拼接的资料请关注编程客栈(www.devze.com)其它相关文章!

    0

    精彩评论

    暂无评论...
    验证码 换一张
    取 消

    关注公众号