系列文章目錄
一篇文章實現Android圖片拼接并保存至相冊
文章目錄
- 系列文章目錄
- 前言
- 實現功能
- 類定義和成員變量
- onCreate方法
- 權限檢查和圖片選擇
- 處理選擇的圖片
- 圖片拼接功能
- 圖片保存功能
- 使用ImageStitcher類拼接圖片
- 代碼解釋:ImageStitcher.java
- 類定義和方法
- 計算拼接后圖片的尺寸
- 計算邏輯
- 創建并繪制拼接后的圖片
- 繪制過程
- 注意事項
- 效果圖
- 源碼
- 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; // 顯示拼接結果的ImageViewprivate 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() {// 創建選擇圖片的IntentIntent 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();
}
使用ImageStitcher類拼接圖片
代碼解釋:ImageStitcher.java
這是一個用于拼接多張圖片的工具類,提供了將多張圖片橫向或縱向拼接成一張大圖的功能。下面是對代碼的詳細解釋:
類定義和方法
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()); // 取最大寬度}}
計算邏輯
- 橫向拼接:總寬度為所有圖片寬度之和,高度為所有圖片中的最大高度
- 縱向拼接:總高度為所有圖片高度之和,寬度為所有圖片中的最大寬度
創建并繪制拼接后的圖片
// 創建拼接后的BitmapBitmap 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,保證圖片質量
- 這是一個基礎實現,沒有處理圖片尺寸不一致時的縮放或裁剪
效果圖
源碼
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;@Overrideprotected 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);}@Overridepublic 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();}}@Overrideprotected 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());}}// 創建拼接后的BitmapBitmap 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"><ProgressBarandroid:id="@+id/jm_progress_bar"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_gravity="center"android:visibility="gone"/><Buttonandroid:id="@+id/jm_select_btn"android:layout_width="wrap_content"android:layout_height="wrap_content"android:text="選擇要拼接的圖片"/><Buttonandroid:id="@+id/jm_stitch_btn"android:layout_width="wrap_content"android:layout_height="wrap_content"android:text="開始拼接圖片"/><Buttonandroid:id="@+id/jm_save_btn"android:layout_width="match_parent"android:layout_height="wrap_content"android:text="保存圖片"android:visibility="gone"/><ImageViewandroid:id="@+id/jm_result_image"android:layout_width="match_parent"android:layout_height="match_parent"android:scaleType="centerInside"/>
</LinearLayout>
總結
此文章可以作為基礎,根據具體需求進行擴展和優化。歡迎留言,如有問題可以聯系計蒙。