Android實現(xiàn)圖片拼接并保存至相冊
前言
好久沒有寫Android系列的文章了,最近有小伙伴問到了Android圖片拼接的問題,寫一篇相關(guān)的博客。
在Android應(yīng)用中實現(xiàn)圖片拼接功能并保存到相冊是一個常見的需求,比如制作全景圖、拼圖應(yīng)用或照片編輯工具。本文將介紹如何實現(xiàn)一個完整的圖片拼接應(yīng)用,包括圖片選擇、拼接和保存功能。
實現(xiàn)功能
- 檢查并請求必要的存儲權(quán)限
- 允許用戶從相冊選擇一張或多張圖片
- 異步加載選中的圖片
- 使用ImageStitcher類拼接圖片
- 將拼接后的圖片保存到相冊
- 在整個過程中顯示適當?shù)倪M度指示和操作反饋
類定義和成員變量
其中包括圖片選擇請求碼,讀取權(quán)限請求碼, 寫入權(quán)限請求碼,保存目錄名稱,以及相關(guān)控件。
public class MainActivity extends AppCompatActivity { private static final int PICK_IMAGE_REQUEST = 1; // 圖片選擇請求碼 private static final int REQUEST_PERMISSION = 2; // 讀取權(quán)限請求碼 private static final int REQUEST_WRITE_PERMISSION = 3; // 寫入權(quán)限請求碼 private static final String SAVE_DIRECTORY = "ImageStitcher"; // 保存目錄名稱 private List<Bitmap> selectedImages = new ArrayList<>(); // 存儲選擇的圖片 private ImageView resultView; // 顯示拼接結(jié)果的ImageView private ProgressBar progressBar; // 進度條 private Button selectBtn, stitchBtn, saveBtn; // 按鈕控件
onCreate方法
初始化控件以及設(shè)置監(jiān)聽
@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è)置按鈕點擊監(jiān)聽器 selectBtn.setOnClickListener(v -> checkPermissionAndOpenChooser()); stitchBtn.setOnClickListener(v -> stitchImagesAsync()); }
權(quán)限檢查和圖片選擇
不動態(tài)申請權(quán)限小心報錯:has no access to content 需在AndroidManifest.xml聲明READ_EXTERNAL_STORAGE權(quán)限,Android Q及以上版本必須使用MediaStore API訪問公共目錄文件。
private void checkPermissionAndOpenChooser() { // 檢查是否有讀取外部存儲權(quán)限 if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) { openImageChooser(); // 有權(quán)限則直接打開圖片選擇器 } else { // 沒有權(quán)限則請求權(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è)置類型為圖片 intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true); // 允許多選 startActivityForResult(Intent.createChooser(intent, "選擇圖片"), PICK_IMAGE_REQUEST); } // 權(quán)限請求結(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)限被授予后打開圖片選擇器 } }
處理選擇的圖片
@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(() -> { // 調(diào)用ImageStitcher類拼接圖片 Bitmap stitched = ImageStitcher.stitchImages( selectedImages.toArray(new Bitmap[0]), 0); runOnUiThread(() -> { resultView.setImageBitmap(stitched); // 顯示拼接結(jié)果 progressBar.setVisibility(View.GONE); // 隱藏進度條 saveBtn.setVisibility(View.VISIBLE); // 確保保存按鈕可見 // 設(shè)置保存按鈕點擊監(jiān)聽器 saveBtn.setOnClickListener(v -> saveImageToGallery(stitched)); }); }); }
圖片保存功能
private void saveImageToGallery(Bitmap bitmap) { // 檢查是否有寫入外部存儲權(quán)限 if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { // 沒有權(quán)限則請求權(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"); // 對于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)) { // 壓縮并寫入圖片數(shù)據(jù) 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) { // 檢查輸入?yún)?shù)是否有效 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()); // 取最大寬度 } }
計算邏輯
橫向拼接:總寬度為所有圖片寬度之和,高度為所有圖片中的最大高度
縱向拼接:總高度為所有圖片高度之和,寬度為所有圖片中的最大寬度
創(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); // 在當前位置繪制圖片 currentPos += image.getWidth(); // 更新橫向位置 } else { // 縱向拼接 canvas.drawBitmap(image, 0, currentPos, null); // 在當前位置繪制圖片 currentPos += image.getHeight(); // 更新縱向位置 } } return result; // 返回拼接后的圖片 } }
繪制過程
- 創(chuàng)建一個新的空白Bitmap,大小為之前計算的總尺寸
- 使用Canvas在這個Bitmap上繪制所有輸入圖片
- 根據(jù)拼接方向,依次將每張圖片繪制到正確的位置
- 更新當前位置指針(currentPos),以便下一張圖片繪制在正確的位置
注意事項
- 所有輸入圖片應(yīng)為非空且尺寸相同(代碼中未做嚴格檢查)
- 拼接方向通過簡單的int值判斷(0為橫向,非0為縱向)
- 使用了ARGB_8888配置創(chuàng)建Bitmap,保證圖片質(zhì)量
- 這是一個基礎(chǔ)實現(xiàn),沒有處理圖片尺寸不一致時的縮放或裁剪
效果圖
源碼
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, "圖片已保存至相冊", 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()); } } // 創(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="開始拼接圖片"/> <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實現(xiàn)圖片拼接并保存至相冊的詳細內(nèi)容,更多關(guān)于Android圖片拼接的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
基于Android studio3.6的JNI教程之helloworld思路詳解
這篇文章主要介紹了基于Android studio3.6的JNI教程之helloworld,本文通過圖文實例代碼給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-03-03Android Data Binding數(shù)據(jù)綁定詳解
本文主要介紹Android Data Binding數(shù)據(jù)綁定的知識,這里整理了詳細的資料及簡單示例代碼幫助大家學習理解此部分知識,有需要的小伙伴可以參考下2016-09-09Android實現(xiàn)仿微軟系統(tǒng)加載動畫效果
這篇文章主要介紹了Android實現(xiàn)仿微軟系統(tǒng)加載動畫效果的方法,幫助大家更好的理解和學習使用Android,感興趣的朋友可以了解下2021-04-04