Android實(shí)現(xiàn)圖片拼接并保存至相冊(cè)
前言
好久沒(méi)有寫(xiě)Android系列的文章了,最近有小伙伴問(wèn)到了Android圖片拼接的問(wèn)題,寫(xiě)一篇相關(guān)的博客。
在Android應(yīng)用中實(shí)現(xiàn)圖片拼接功能并保存到相冊(cè)是一個(gè)常見(jiàn)的需求,比如制作全景圖、拼圖應(yīng)用或照片編輯工具。本文將介紹如何實(shí)現(xiàn)一個(gè)完整的圖片拼接應(yīng)用,包括圖片選擇、拼接和保存功能。
實(shí)現(xiàn)功能
- 檢查并請(qǐng)求必要的存儲(chǔ)權(quán)限
- 允許用戶從相冊(cè)選擇一張或多張圖片
- 異步加載選中的圖片
- 使用ImageStitcher類(lèi)拼接圖片
- 將拼接后的圖片保存到相冊(cè)
- 在整個(gè)過(guò)程中顯示適當(dāng)?shù)倪M(jìn)度指示和操作反饋
類(lèi)定義和成員變量
其中包括圖片選擇請(qǐng)求碼,讀取權(quán)限請(qǐng)求碼, 寫(xiě)入權(quán)限請(qǐng)求碼,保存目錄名稱(chēng),以及相關(guān)控件。
public class MainActivity extends AppCompatActivity {
private static final int PICK_IMAGE_REQUEST = 1; // 圖片選擇請(qǐng)求碼
private static final int REQUEST_PERMISSION = 2; // 讀取權(quán)限請(qǐng)求碼
private static final int REQUEST_WRITE_PERMISSION = 3; // 寫(xiě)入權(quán)限請(qǐng)求碼
private static final String SAVE_DIRECTORY = "ImageStitcher"; // 保存目錄名稱(chēng)
private List<Bitmap> selectedImages = new ArrayList<>(); // 存儲(chǔ)選擇的圖片
private ImageView resultView; // 顯示拼接結(jié)果的ImageView
private ProgressBar progressBar; // 進(jìn)度條
private Button selectBtn, stitchBtn, saveBtn; // 按鈕控件
onCreate方法
初始化控件以及設(shè)置監(jiān)聽(tīng)
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main); // 設(shè)置布局文件
// 初始化視圖控件
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); // 初始時(shí)隱藏保存按鈕
// 設(shè)置按鈕點(diǎn)擊監(jiān)聽(tīng)器
selectBtn.setOnClickListener(v -> checkPermissionAndOpenChooser());
stitchBtn.setOnClickListener(v -> stitchImagesAsync());
}
權(quán)限檢查和圖片選擇
不動(dòng)態(tài)申請(qǐng)權(quán)限小心報(bào)錯(cuò):has no access to content 需在AndroidManifest.xml聲明READ_EXTERNAL_STORAGE權(quán)限,Android Q及以上版本必須使用MediaStore API訪問(wèn)公共目錄文件。
private void checkPermissionAndOpenChooser() {
// 檢查是否有讀取外部存儲(chǔ)權(quán)限
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE)
== PackageManager.PERMISSION_GRANTED) {
openImageChooser(); // 有權(quán)限則直接打開(kāi)圖片選擇器
} else {
// 沒(méi)有權(quán)限則請(qǐng)求權(quán)限
ActivityCompat.requestPermissions(this,
new String[]{Manifest.permission.READ_EXTERNAL_STORAGE},
REQUEST_PERMISSION);
}
}
private void openImageChooser() {
// 創(chuàng)建選擇圖片的Intent
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.setType("image/*"); // 設(shè)置類(lèi)型為圖片
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true); // 允許多選
startActivityForResult(Intent.createChooser(intent, "選擇圖片"), PICK_IMAGE_REQUEST);
}
// 權(quán)限請(qǐng)求結(jié)果回調(diào)
@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(); // 權(quán)限被授予后打開(kāi)圖片選擇器
}
}
處理選擇的圖片
@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); // 顯示進(jìn)度條
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)); // 隱藏進(jìn)度條
}
});
}
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; // 如果沒(méi)有選擇圖片則返回
saveBtn.setVisibility(View.VISIBLE); // 顯示保存按鈕
progressBar.setVisibility(View.VISIBLE); // 顯示進(jìn)度條
ExecutorService executor = Executors.newSingleThreadExecutor();
executor.execute(() -> {
// 調(diào)用ImageStitcher類(lèi)拼接圖片
Bitmap stitched = ImageStitcher.stitchImages(
selectedImages.toArray(new Bitmap[0]), 0);
runOnUiThread(() -> {
resultView.setImageBitmap(stitched); // 顯示拼接結(jié)果
progressBar.setVisibility(View.GONE); // 隱藏進(jìn)度條
saveBtn.setVisibility(View.VISIBLE); // 確保保存按鈕可見(jiàn)
// 設(shè)置保存按鈕點(diǎn)擊監(jiān)聽(tīng)器
saveBtn.setOnClickListener(v -> saveImageToGallery(stitched));
});
});
}
圖片保存功能
private void saveImageToGallery(Bitmap bitmap) {
// 檢查是否有寫(xiě)入外部存儲(chǔ)權(quán)限
if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED) {
// 沒(méi)有權(quán)限則請(qǐng)求權(quán)限
ActivityCompat.requestPermissions(this,
new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},
REQUEST_WRITE_PERMISSION);
return;
}
// 在新線程中執(zhí)行保存操作
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");
// 對(duì)于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 {
// 插入媒體庫(kù)記錄
Uri uri = getContentResolver().insert(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
try (OutputStream os = getContentResolver().openOutputStream(uri)) {
// 壓縮并寫(xiě)入圖片數(shù)據(jù)
bitmap.compress(Bitmap.CompressFormat.JPEG, 90, os);
// 對(duì)于Android Q及以上版本,更新IS_PENDING標(biāo)志
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, "圖片已保存至相冊(cè)", Toast.LENGTH_SHORT).show());
}
} catch (Exception e) {
// 顯示保存失敗提示
runOnUiThread(() ->
Toast.makeText(this, "保存失敗: " + e.getMessage(), Toast.LENGTH_SHORT).show());
}
}).start();
}
使用ImageStitcher類(lèi)拼接圖片
代碼解釋?zhuān)篒mageStitcher.java
這是一個(gè)用于拼接多張圖片的工具類(lèi),提供了將多張圖片橫向或縱向拼接成一張大圖的功能。下面是對(duì)代碼的詳細(xì)解釋?zhuān)?/p>
類(lèi)定義和方法
public class ImageStitcher {
public static Bitmap stitchImages(Bitmap[] images, int direction) {
// 檢查輸入?yún)?shù)是否有效
if (images == null || images.length == 0) return null;
計(jì)算拼接后圖片的尺寸
int width = images[0].getWidth();
int height = images[0].getHeight();
// 計(jì)算拼接后圖片的總尺寸
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()); // 取最大寬度
}
}
計(jì)算邏輯
橫向拼接:總寬度為所有圖片寬度之和,高度為所有圖片中的最大高度
縱向拼接:總高度為所有圖片高度之和,寬度為所有圖片中的最大寬度
創(chuàng)建并繪制拼接后的圖片
// 創(chuàng)建拼接后的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); // 在當(dāng)前位置繪制圖片
currentPos += image.getWidth(); // 更新橫向位置
} else { // 縱向拼接
canvas.drawBitmap(image, 0, currentPos, null); // 在當(dāng)前位置繪制圖片
currentPos += image.getHeight(); // 更新縱向位置
}
}
return result; // 返回拼接后的圖片
}
}
繪制過(guò)程
- 創(chuàng)建一個(gè)新的空白Bitmap,大小為之前計(jì)算的總尺寸
- 使用Canvas在這個(gè)Bitmap上繪制所有輸入圖片
- 根據(jù)拼接方向,依次將每張圖片繪制到正確的位置
- 更新當(dāng)前位置指針(currentPos),以便下一張圖片繪制在正確的位置
注意事項(xiàng)
- 所有輸入圖片應(yīng)為非空且尺寸相同(代碼中未做嚴(yán)格檢查)
- 拼接方向通過(guò)簡(jiǎn)單的int值判斷(0為橫向,非0為縱向)
- 使用了ARGB_8888配置創(chuàng)建Bitmap,保證圖片質(zhì)量
- 這是一個(gè)基礎(chǔ)實(shí)現(xiàn),沒(méi)有處理圖片尺寸不一致時(shí)的縮放或裁剪
效果圖

源碼
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);
//設(shè)置出現(xiàn)
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, "圖片已保存至相冊(cè)", 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();
// 計(jì)算拼接后圖片的總尺寸
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());
}
}
// 創(chuàng)建拼接后的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權(quán)限申明
<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="開(kāi)始拼接圖片"/>
<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實(shí)現(xiàn)圖片拼接并保存至相冊(cè)的詳細(xì)內(nèi)容,更多關(guān)于Android圖片拼接的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
基于Android studio3.6的JNI教程之helloworld思路詳解
這篇文章主要介紹了基于Android studio3.6的JNI教程之helloworld,本文通過(guò)圖文實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-03-03
Flutter自動(dòng)路由插件auto_route使用詳解
這篇文章主要為大家介紹了Flutter自動(dòng)路由插件auto_route的基本使用詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-08-08
Android Data Binding數(shù)據(jù)綁定詳解
本文主要介紹Android Data Binding數(shù)據(jù)綁定的知識(shí),這里整理了詳細(xì)的資料及簡(jiǎn)單示例代碼幫助大家學(xué)習(xí)理解此部分知識(shí),有需要的小伙伴可以參考下2016-09-09
Android實(shí)現(xiàn)仿微軟系統(tǒng)加載動(dòng)畫(huà)效果
這篇文章主要介紹了Android實(shí)現(xiàn)仿微軟系統(tǒng)加載動(dòng)畫(huà)效果的方法,幫助大家更好的理解和學(xué)習(xí)使用Android,感興趣的朋友可以了解下2021-04-04

