好久没有写Android系列的文章了,最近有小伙伴问到了Android图片拼接的问题,写一篇相关的博客。
在Android应用中实现图片拼接功能并保存到相册是一个常见的需求,比如制作全景图、拼图应用或照片编辑工具。本文将介绍如何实现一个完整的图片拼接应用,包括图片选择、拼接和保存功能。
其中包括图片选择请求码,读取权限请求码, 写入权限请求码,保存目录名称,以及相关控件。
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; // 按钮控件
初始化控件以及设置监听
@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 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());
}
}
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();
}
这是一个用于拼接多张图片的工具类,提供了将多张图片横向或纵向拼接成一张大图的功能。下面是对代码的详细解释:
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; // 返回拼接后的图片
}
}
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();
}
}
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;
}
}
<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" />
<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>
此文章可以作为基础,根据具体需求进行扩展和优化。