-
Notifications
You must be signed in to change notification settings - Fork 165
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'main' into top-components
- Loading branch information
Showing
7 changed files
with
279 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
266 changes: 266 additions & 0 deletions
266
.../snippets/src/main/java/com/example/compose/snippets/graphics/AdvancedGraphicsSnippets.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,266 @@ | ||
/* | ||
* Copyright 2023 The Android Open Source Project | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* https://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
package com.example.compose.snippets.graphics | ||
|
||
import android.Manifest | ||
import android.content.Context | ||
import android.content.Intent | ||
import android.content.Intent.createChooser | ||
import android.graphics.Bitmap | ||
import android.graphics.Picture | ||
import android.media.MediaScannerConnection | ||
import android.net.Uri | ||
import android.os.Build | ||
import android.os.Environment | ||
import android.provider.MediaStore | ||
import androidx.compose.foundation.Image | ||
import androidx.compose.foundation.background | ||
import androidx.compose.foundation.layout.Arrangement | ||
import androidx.compose.foundation.layout.Column | ||
import androidx.compose.foundation.layout.aspectRatio | ||
import androidx.compose.foundation.layout.fillMaxSize | ||
import androidx.compose.foundation.layout.padding | ||
import androidx.compose.material.icons.Icons | ||
import androidx.compose.material.icons.filled.Share | ||
import androidx.compose.material3.FloatingActionButton | ||
import androidx.compose.material3.Icon | ||
import androidx.compose.material3.Scaffold | ||
import androidx.compose.material3.SnackbarHost | ||
import androidx.compose.material3.SnackbarHostState | ||
import androidx.compose.material3.SnackbarResult | ||
import androidx.compose.material3.Text | ||
import androidx.compose.runtime.Composable | ||
import androidx.compose.runtime.remember | ||
import androidx.compose.runtime.rememberCoroutineScope | ||
import androidx.compose.ui.Alignment | ||
import androidx.compose.ui.Modifier | ||
import androidx.compose.ui.draw.drawWithCache | ||
import androidx.compose.ui.graphics.Brush | ||
import androidx.compose.ui.graphics.Color | ||
import androidx.compose.ui.graphics.drawscope.draw | ||
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas | ||
import androidx.compose.ui.graphics.nativeCanvas | ||
import androidx.compose.ui.layout.ContentScale | ||
import androidx.compose.ui.platform.LocalContext | ||
import androidx.compose.ui.res.painterResource | ||
import androidx.compose.ui.tooling.preview.Preview | ||
import androidx.compose.ui.unit.dp | ||
import androidx.compose.ui.unit.sp | ||
import androidx.core.content.ContextCompat.startActivity | ||
import com.example.compose.snippets.R | ||
import com.google.accompanist.permissions.ExperimentalPermissionsApi | ||
import com.google.accompanist.permissions.rememberMultiplePermissionsState | ||
import java.io.File | ||
import kotlin.coroutines.resume | ||
import kotlinx.coroutines.Dispatchers | ||
import kotlinx.coroutines.launch | ||
import kotlinx.coroutines.suspendCancellableCoroutine | ||
|
||
/* | ||
* Copyright 2022 The Android Open Source Project | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
@OptIn(ExperimentalPermissionsApi::class) | ||
@Preview | ||
@Composable | ||
fun BitmapFromComposableSnippet() { | ||
val context = LocalContext.current | ||
val coroutineScope = rememberCoroutineScope() | ||
val snackbarHostState = remember { SnackbarHostState() } | ||
val picture = remember { Picture() } | ||
|
||
val writeStorageAccessState = rememberMultiplePermissionsState( | ||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { | ||
// No permissions are needed on Android 10+ to add files in the shared storage | ||
emptyList() | ||
} else { | ||
listOf(Manifest.permission.WRITE_EXTERNAL_STORAGE) | ||
} | ||
) | ||
// This logic should live in your ViewModel - trigger a side effect to invoke URI sharing. | ||
// checks permissions granted, and then saves the bitmap from a Picture that is already capturing content | ||
// and shares it with the default share sheet. | ||
fun shareBitmapFromComposable() { | ||
if (writeStorageAccessState.allPermissionsGranted) { | ||
coroutineScope.launch(Dispatchers.IO) { | ||
val bitmap = createBitmapFromPicture(picture) | ||
val uri = bitmap.saveToDisk(context) | ||
shareBitmap(context, uri) | ||
} | ||
} else if (writeStorageAccessState.shouldShowRationale) { | ||
coroutineScope.launch { | ||
val result = snackbarHostState.showSnackbar( | ||
message = "The storage permission is needed to save the image", | ||
actionLabel = "Grant Access" | ||
) | ||
|
||
if (result == SnackbarResult.ActionPerformed) { | ||
writeStorageAccessState.launchMultiplePermissionRequest() | ||
} | ||
} | ||
} else { | ||
writeStorageAccessState.launchMultiplePermissionRequest() | ||
} | ||
} | ||
|
||
Scaffold( | ||
snackbarHost = { SnackbarHost(snackbarHostState) }, | ||
floatingActionButton = { | ||
FloatingActionButton(onClick = { | ||
shareBitmapFromComposable() | ||
}) { | ||
Icon(Icons.Default.Share, "share") | ||
} | ||
} | ||
) { padding -> | ||
// [START android_compose_draw_into_bitmap] | ||
Column( | ||
modifier = Modifier | ||
.padding(padding) | ||
.fillMaxSize() | ||
.drawWithCache { | ||
// Example that shows how to redirect rendering to an Android Picture and then | ||
// draw the picture into the original destination | ||
val width = this.size.width.toInt() | ||
val height = this.size.height.toInt() | ||
|
||
onDrawWithContent { | ||
val pictureCanvas = | ||
androidx.compose.ui.graphics.Canvas( | ||
picture.beginRecording( | ||
width, | ||
height | ||
) | ||
) | ||
draw(this, this.layoutDirection, pictureCanvas, this.size) { | ||
this@onDrawWithContent.drawContent() | ||
} | ||
picture.endRecording() | ||
|
||
drawIntoCanvas { canvas -> canvas.nativeCanvas.drawPicture(picture) } | ||
} | ||
} | ||
) { | ||
ScreenContentToCapture() | ||
} | ||
// [END android_compose_draw_into_bitmap] | ||
} | ||
} | ||
|
||
@Composable | ||
private fun ScreenContentToCapture() { | ||
Column( | ||
horizontalAlignment = Alignment.CenterHorizontally, | ||
verticalArrangement = Arrangement.Center, | ||
modifier = Modifier | ||
.fillMaxSize() | ||
.background( | ||
Brush.linearGradient( | ||
listOf( | ||
Color(0xFFF5D5C0), | ||
Color(0xFFF8E8E3) | ||
) | ||
) | ||
) | ||
) { | ||
Image( | ||
painterResource(id = R.drawable.sunset), | ||
contentDescription = null, | ||
modifier = Modifier | ||
.aspectRatio(1f) | ||
.padding(32.dp), | ||
contentScale = ContentScale.Crop | ||
) | ||
Text( | ||
"Into the Ocean depths", | ||
fontSize = 18.sp | ||
) | ||
} | ||
} | ||
|
||
private fun createBitmapFromPicture(picture: Picture): Bitmap { | ||
// [START android_compose_draw_into_bitmap_convert_picture] | ||
val bitmap = Bitmap.createBitmap( | ||
picture.width, | ||
picture.height, | ||
Bitmap.Config.ARGB_8888 | ||
) | ||
|
||
val canvas = android.graphics.Canvas(bitmap) | ||
canvas.drawColor(android.graphics.Color.WHITE) | ||
canvas.drawPicture(picture) | ||
// [END android_compose_draw_into_bitmap_convert_picture] | ||
return bitmap | ||
} | ||
|
||
private suspend fun Bitmap.saveToDisk(context: Context): Uri { | ||
val file = File( | ||
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES), | ||
"screenshot-${System.currentTimeMillis()}.png" | ||
) | ||
|
||
file.writeBitmap(this, Bitmap.CompressFormat.PNG, 100) | ||
|
||
return scanFilePath(context, file.path) ?: throw Exception("File could not be saved") | ||
} | ||
|
||
/** | ||
* We call [MediaScannerConnection] to index the newly created image inside MediaStore to be visible | ||
* for other apps, as well as returning its [MediaStore] Uri | ||
*/ | ||
private suspend fun scanFilePath(context: Context, filePath: String): Uri? { | ||
return suspendCancellableCoroutine { continuation -> | ||
MediaScannerConnection.scanFile( | ||
context, | ||
arrayOf(filePath), | ||
arrayOf("image/png") | ||
) { _, scannedUri -> | ||
if (scannedUri == null) { | ||
continuation.cancel(Exception("File $filePath could not be scanned")) | ||
} else { | ||
continuation.resume(scannedUri) | ||
} | ||
} | ||
} | ||
} | ||
|
||
private fun File.writeBitmap(bitmap: Bitmap, format: Bitmap.CompressFormat, quality: Int) { | ||
outputStream().use { out -> | ||
bitmap.compress(format, quality, out) | ||
out.flush() | ||
} | ||
} | ||
|
||
private fun shareBitmap(context: Context, uri: Uri) { | ||
val intent = Intent(Intent.ACTION_SEND).apply { | ||
type = "image/png" | ||
putExtra(Intent.EXTRA_STREAM, uri) | ||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) | ||
} | ||
startActivity(context, createChooser(intent, "Share your image"), null) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters