Skip to content

Commit

Permalink
Merge branch 'main' into top-components
Browse files Browse the repository at this point in the history
  • Loading branch information
jakeroseman authored Aug 24, 2023
2 parents e6bcf18 + bcbbd86 commit 466a904
Show file tree
Hide file tree
Showing 7 changed files with 279 additions and 2 deletions.
2 changes: 2 additions & 0 deletions compose/snippets/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,8 @@ dependencies {
implementation(libs.accompanist.theme.adapter.material3)
implementation(libs.accompanist.theme.adapter.material)

implementation(libs.accompanist.permissions)

implementation(libs.coil.kt.compose)
implementation(libs.androidx.activity.compose)
implementation(libs.androidx.appcompat)
Expand Down
4 changes: 3 additions & 1 deletion compose/snippets/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="28" />

<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
Expand Down Expand Up @@ -57,7 +60,6 @@
android:resource="@xml/my_app_widget_info" />
</receiver>
<!-- [END android_compose_glance_declare] -->

</application>

</manifest>
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package com.example.compose.snippets

import android.os.Bundle
import android.os.StrictMode
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.fillMaxSize
Expand All @@ -37,6 +38,7 @@ import com.example.compose.snippets.components.ProgressIndicatorExamples
import com.example.compose.snippets.components.ScaffoldExample
import com.example.compose.snippets.components.SliderExamples
import com.example.compose.snippets.components.SwitchExamples
import com.example.compose.snippets.graphics.BitmapFromComposableSnippet
import com.example.compose.snippets.graphics.BrushExamplesScreen
import com.example.compose.snippets.images.ImageExamplesScreen
import com.example.compose.snippets.landing.LandingScreen
Expand All @@ -48,6 +50,7 @@ import com.example.topcomponents.CardExamples
class SnippetsActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
StrictMode.enableDefaults()
setContent {
SnippetsTheme {
val navController = rememberNavController()
Expand All @@ -66,6 +69,7 @@ class SnippetsActivity : ComponentActivity() {
Destination.BrushExamples -> BrushExamplesScreen()
Destination.ImageExamples -> ImageExamplesScreen()
Destination.AnimationQuickGuideExamples -> AnimationExamplesScreen()
Destination.ScreenshotExample -> BitmapFromComposableSnippet()
Destination.ComponentsExamples -> ComponentsScreen {
navController.navigate(
it.route
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ fun LinearDeterminateIndicator() {
modifier = Modifier.fillMaxWidth(),
progress = currentProgress
)
LinearProgressIndicator(progress = currentProgress)
}
}
}
Expand Down Expand Up @@ -133,6 +134,7 @@ fun CircularDeterminateIndicator() {
modifier = Modifier.width(64.dp),
progress = currentProgress
)
CircularProgressIndicator(progress = currentProgress)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -610,7 +610,7 @@ object SizeModeSnippets3 {

@Composable
private fun MyContent() {
// Size will be the max available size for the AppWidget
// Size will be the size of the AppWidget
val size = LocalSize.current
Column {
Text(text = "Where to?", modifier = GlanceModifier.padding(12.dp))
Expand Down
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)
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ enum class Destination(val route: String, val title: String) {
ImageExamples("imageExamples", "Image Examples"),
AnimationQuickGuideExamples("animationExamples", "Animation Examples"),
ComponentsExamples("topComponents", "Top Compose Components"),
ScreenshotExample("screenshotExample", "Screenshot Examples"),
}

// Enum class for compose components navigation screen.
Expand Down

0 comments on commit 466a904

Please sign in to comment.