#java #android #canvas #bitmap #scoped-storage
Вопрос:
Я добавлял новую функцию в свой проект приложения для рисования, где пользователи могли открывать любую картинку из телефонной галереи и рисовать на ней. Но у меня возникли проблемы с сохранением нового растрового изображения. Вот подробная информация ниже :
В моем приложении есть 3 вида деятельности :
- Основное действие в виде меню,
- Действие RecyclerView, которое действует как галерея,
- Рисование, в котором пользователи рисуют
До недавнего времени всякий раз, когда пользователь сохранял изображение, он создавал две его копии. Одно частное, которое будет храниться в личном хранилище приложения, недоступном для использования в галерее телефона, но отображается в активности собственной галереи приложения, и одно общедоступное изображение в средствах массовой информации, которое будет отображаться в галерее телефонов. Мой класс галереи проверяет внутреннюю папку приложения и перечисляет все файлы в ней для отображения, поэтому я также сохраняю их в личном каталоге приложения. Метод сохранения изображения состоит из двух разделов, которые решают проблемы с областью хранения с помощью api 28 и
Недавно я добавил функцию, в которой пользователи могут выбрать любую фотографию из галереи телефона и отредактировать ее (как изображения, созданные приложением, так и те, которые оно не создавало).Он использует функцию выбора намерения.
Ожидаемыми результатами являются :
- Откройте картинку из галереи в упражнении рисования и нарисуйте поверх нее
- Сохраните отредактированную версию в виде нового изображения как в частном порядке в хранилище приложения, так и в общедоступной общей копии в главной галерее телефонов
Текущие проблемы :
- Изображение открывается и рисуется без проблем (как изображения, сделанные приложением, так и иностранные изображения)
- Сохранение изображений приводит к тому, что приложение сохраняет пустое растровое изображение в частной галерее телефона и изменения (внесенные изменения) в общем хранилище на телефоне без фактического изображения, которое должно было быть отредактировано.
Вот код :
От MainActivty-код, который открывает изображения из галереи телефона для их редактирования.
// checks permissions for reading external and scoped storage before opening image picker
private void pickAnImage() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q amp;amp; checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED) {
Log.d(TAG, "SDK >= Q , requesting permission");
requestPermissions(new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, 1000);
} else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q amp;amp; checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED) {
Log.d(TAG, "SDK < Q , requesting permission");
requestPermissions(new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, 1000);
} else {
Log.d(TAG, "Permission exists, starting pick intent");
Intent gallery = new Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
startActivityForResult(gallery, IMAGE);
}
}
// handles results from permissions and begins intent picker
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == 1000) {
Log.d(TAG, "result code good");
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
Log.d(TAG, "Permission is granted, starting pick");
Intent gallery = new Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
startActivityForResult(gallery, IMAGE);
} else {
Log.d(TAG, "While result code is good, permission was denied");
Toast.makeText(MainActivity.this, "Permission Denied !", Toast.LENGTH_SHORT).show();
}
}
}
// handles results from intent picker and feeds the image path to the drawing activity for image editing
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (resultCode == RESULT_OK amp;amp; requestCode == IMAGE amp;amp; data != null) {
Log.d(TAG, "Data from picked image is not null, creating uri and path");
Uri selectedImageUri = data.getData();
String picturePath = getPath(getApplicationContext(), selectedImageUri);
// Log.d("Picture Path", picturePath);
if (picturePath != null) {
Log.d(TAG, "Path creating success, calling art activity");
Intent intent = new Intent(getApplicationContext(), ArtActivity.class);
intent.putExtra("image", picturePath);
startActivity(intent);
} else {
Log.d(TAG, "Path was null");
finish();
}
}else{
Log.d(TAG, "Data seems to be null, aborting");
}
}
// obtains the path from the picked image
private static String getPath(Context context, Uri uri) {
String result = null;
String[] proj = {MediaStore.Images.Media.DATA};
Cursor cursor = context.getContentResolver().query(uri, proj, null, null, null);
if (cursor != null) {
if (cursor.moveToFirst()) {
int column_index = cursor.getColumnIndexOrThrow(proj[0]);
result = cursor.getString(column_index);
Toast.makeText(context, "" result, Toast.LENGTH_SHORT).show();
}
cursor.close();
}
else {
Toast.makeText(context.getApplicationContext(), "Failed to get image path , result is null or permission problem ?", Toast.LENGTH_SHORT).show();
result = "Not found";
Toast.makeText(context, "" result, Toast.LENGTH_SHORT).show();
}
return resu<
}
Below are methods that handle bitmap editing process :
// Inside initialization method that sets up paint and bitmap objects before drawing
if (bitmap != null) {
// if bitmap object is not null it means that we are feeding existing bitmap to be edited, therefore we just scale it to screen size before giving it to canvas
loadedBitmap = scale(bitmap, width, height);
} else {
// bitmap fed to this method is null therefore it means we are not editing existing picture so we create a new object to draw on
this.bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888, true);
}
Inside onDraw method that handles drawing as it happens :
if (loadedBitmap != null) {
// loadedBitmap not being null means we are editing existing image from above, we previously scaled it so now we feed it to the canvas for editing
canvas.drawBitmap(loadedBitmap, 0, 0, paintLine);
canvas.clipRect(0, 0, loadedBitmap.getWidth(), loadedBitmap.getHeight());
} else {
// we are not editing an image so we draw new empty bitmap to use
canvas.drawBitmap(bitmap, 0, 0, tPaintline);
}
And finally, here is the infamous save image method that is the root of current problems :
@SuppressLint("WrongThread") // not sure what to do otherwise about this lint
public void saveImage() throws IOException {
//create a filename and canvas
String filename = "appName" System.currentTimeMillis();
Canvas canvas;
// loadedBitmap is the edited one, bitmap is a new one if we did not edit anything
if(loadedBitmap != null){
canvas = new Canvas(loadedBitmap);
} else{
canvas = new Canvas(bitmap);
}
// save image handle for newer api that has scoped storage and updated code
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
// feed all the data to content values
OutputStream fos;
ContentResolver resolver = context.getContentResolver();
ContentValues contentValues = new ContentValues();
contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, filename ".jpg");
contentValues.put(MediaStore.MediaColumns.MIME_TYPE, "image/jpg");
contentValues.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_PICTURES);
Uri imageUri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues);
fos = resolver.openOutputStream(Objects.requireNonNull(imageUri));
draw(canvas);
// compress correct bitmap, loaded is edited, bitmap is new art
if(loadedBitmap != null){
loadedBitmap.compress(Bitmap.CompressFormat.PNG, 100, fos);
}else{
bitmap.compress(Bitmap.CompressFormat.PNG, 100, fos);
}
Objects.requireNonNull(fos).close();
imageSaved = true;
} else {
// for api older then 28 before scoped storage
// create app directory and a new image file
ContextWrapper cw = new ContextWrapper(getContext());
File directory = cw.getDir("files", Context.MODE_PRIVATE);
pathString = cw.getDir("files", Context.MODE_PRIVATE).toString();
File myPath = new File(directory, filename ".jpg");
FileOutputStream fileOutputStream = new FileOutputStream(myPath);
// check permissions
try {
if(ContextCompat.checkSelfPermission(
context, Manifest.permission.WRITE_EXTERNAL_STORAGE) ==
PackageManager.PERMISSION_GRANTED){
draw(canvas);
// save edited or new bitmap to private internal storage
if(loadedBitmap != null){
MediaStore.Images.Media.insertImage(context.getContentResolver(), loadedBitmap, filename, "made with appName");
}else{
MediaStore.Images.Media.insertImage(context.getContentResolver(), bitmap, filename, "made with appName");
}
// now also add a copy to shared storage so it will show up in phone's gallery
addImageToGallery(myPath.getPath(), context);
imageSaved = true;
}else{
// request permissions if not available
requestPermissions((Activity) context,
new String[] { Manifest.permission.WRITE_EXTERNAL_STORAGE },
100);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
fileOutputStream.flush();
fileOutputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
Save image to gallery method used above :
// for old api <28, it adds the image to shared storage and phone's gallery
public static void addImageToGallery(final String filePath, final Context context) {
ContentValues values = new ContentValues();
values.put(MediaStore.Images.Media.DATE_TAKEN, System.currentTimeMillis());
values.put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg");
values.put(MediaStore.MediaColumns.DATA, filePath);
context.getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
}
Вот и все, если кто-нибудь знает растровое изображение, холст и разрешения (старые и новые), не стесняйтесь помочь. Я уверен, что изменения, которые мне нужны, невелики. Проверьте текущие проблемы выше, чтобы знать, что искать.