parent
7d45eae4f8
commit
fb4a0b25f6
@ -0,0 +1,22 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!--
|
||||||
|
~ Copyright 2026 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.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<paths>
|
||||||
|
<cache-path name="my_cache" path="." />
|
||||||
|
<external-path name="my_external" path="." />
|
||||||
|
<cache-path name="shared_images" path="shared_images/" />
|
||||||
|
</paths>
|
||||||
@ -0,0 +1,10 @@
|
|||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
|
<application>
|
||||||
|
<activity
|
||||||
|
android:name="com.google.samples.apps.nowinandroid.feature.interests.impl.filter.FilterActivity"
|
||||||
|
android:exported="false"
|
||||||
|
android:theme="@android:style/Theme.Material.Light.NoActionBar"
|
||||||
|
android:windowSoftInputMode="adjustResize" />
|
||||||
|
</application>
|
||||||
|
</manifest>
|
||||||
@ -0,0 +1,201 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2026 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.google.samples.apps.nowinandroid.feature.interests.impl.filter
|
||||||
|
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.activity.ComponentActivity
|
||||||
|
import androidx.activity.compose.setContent
|
||||||
|
import androidx.activity.enableEdgeToEdge
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.lazy.LazyRow
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.ImageBitmap
|
||||||
|
import androidx.compose.ui.graphics.asImageBitmap
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme
|
||||||
|
|
||||||
|
internal const val IO_CONNECT_WORKSHOP_TOPIC_NAME = "IO connect Workshop"
|
||||||
|
|
||||||
|
class FilterActivity : ComponentActivity() {
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
enableEdgeToEdge()
|
||||||
|
setContent {
|
||||||
|
NiaTheme {
|
||||||
|
Scaffold(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
) { innerPadding ->
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(innerPadding).padding(top = 40.dp, bottom = 16.dp, start = 16.dp, end = 16.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Apply Filters Use Case",
|
||||||
|
style = MaterialTheme.typography.titleLarge,
|
||||||
|
color = MaterialTheme.colorScheme.onErrorContainer,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
|
||||||
|
FilterScreen(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun FilterScreen(modifier: Modifier = Modifier) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
var selectedFilter by remember { mutableStateOf(FILTERS.first()) }
|
||||||
|
|
||||||
|
val sourceBitmap = remember { decodeSourceBitmap(context) }
|
||||||
|
|
||||||
|
val previewBitmap = createFilteredBitmap(sourceBitmap, selectedFilter)
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
|
.padding(16.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = "Preview: ${selectedFilter.name}",
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
fontWeight = FontWeight.SemiBold
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
|
||||||
|
Image(
|
||||||
|
bitmap = previewBitmap.asImageBitmap(),
|
||||||
|
contentDescription = "Filtered Preview (${selectedFilter.name})",
|
||||||
|
modifier = Modifier
|
||||||
|
.size(300.dp)
|
||||||
|
.clip(RoundedCornerShape(12.dp))
|
||||||
|
.background(Color.LightGray),
|
||||||
|
contentScale = ContentScale.Crop
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
|
||||||
|
LazyRow(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||||
|
items(FILTERS, key = { it.name }) { filter ->
|
||||||
|
val thumbnail = createFilteredBitmap(sourceBitmap, filter)
|
||||||
|
FilterThumbnail(
|
||||||
|
image = thumbnail.asImageBitmap(),
|
||||||
|
name = filter.name,
|
||||||
|
selected = filter == selectedFilter,
|
||||||
|
onClick = { selectedFilter = filter}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
val sharableBitmap = createFilteredBitmap(sourceBitmap, selectedFilter)
|
||||||
|
shareBitmap(context, sharableBitmap, selectedFilter.name)
|
||||||
|
},
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Text(text = "Share filtered Image")
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun FilterThumbnail(
|
||||||
|
image: ImageBitmap,
|
||||||
|
name: String,
|
||||||
|
selected: Boolean,
|
||||||
|
onClick: () -> Unit
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
modifier = Modifier.clickable(onClick = onClick)
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(84.dp)
|
||||||
|
.clip(RoundedCornerShape(8.dp))
|
||||||
|
.border(
|
||||||
|
width = if (selected) 3.dp else 1.dp,
|
||||||
|
color = if (selected) MaterialTheme.colorScheme.primary else Color.Gray,
|
||||||
|
shape = RoundedCornerShape(8.dp)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
bitmap = image,
|
||||||
|
contentDescription = name,
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentScale = ContentScale.Crop
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
Text(
|
||||||
|
text = name,
|
||||||
|
style = MaterialTheme.typography.labelMedium,
|
||||||
|
fontWeight = if (selected) FontWeight.Bold else FontWeight.Normal
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,126 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2026 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.google.samples.apps.nowinandroid.feature.interests.impl.filter
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.BitmapFactory
|
||||||
|
import android.graphics.Canvas
|
||||||
|
import android.graphics.ColorMatrix
|
||||||
|
import android.graphics.ColorMatrixColorFilter
|
||||||
|
import android.graphics.Paint
|
||||||
|
import androidx.core.content.FileProvider
|
||||||
|
import com.google.samples.apps.nowinandroid.feature.interests.impl.R
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileOutputStream
|
||||||
|
|
||||||
|
class ImageFilter(
|
||||||
|
val name: String,
|
||||||
|
val matrix: FloatArray
|
||||||
|
)
|
||||||
|
|
||||||
|
val FILTERS = listOf(
|
||||||
|
ImageFilter(
|
||||||
|
"Original", floatArrayOf(
|
||||||
|
1f, 0f, 0f, 0f, 0f,
|
||||||
|
0f, 1f, 0f, 0f, 0f,
|
||||||
|
0f, 0f, 1f, 0f, 0f,
|
||||||
|
0f, 0f, 0f, 1f, 0f
|
||||||
|
)
|
||||||
|
),
|
||||||
|
ImageFilter(
|
||||||
|
"Grayscale", floatArrayOf(
|
||||||
|
0.299f, 0.587f, 0.114f, 0f, 0f,
|
||||||
|
0.299f, 0.587f, 0.114f, 0f, 0f,
|
||||||
|
0.299f, 0.587f, 0.114f, 0f, 0f,
|
||||||
|
0f, 0f, 0f, 1f, 0f
|
||||||
|
)
|
||||||
|
),
|
||||||
|
ImageFilter(
|
||||||
|
"Sepia", floatArrayOf(
|
||||||
|
0.393f, 0.769f, 0.189f, 0f, 0f,
|
||||||
|
0.349f, 0.686f, 0.168f, 0f, 0f,
|
||||||
|
0.272f, 0.534f, 0.131f, 0f, 0f,
|
||||||
|
0f, 0f, 0f, 1f, 0f
|
||||||
|
)
|
||||||
|
),
|
||||||
|
ImageFilter(
|
||||||
|
"Invert", floatArrayOf(
|
||||||
|
-1f, 0f, 0f, 0f, 255f,
|
||||||
|
0f, -1f, 0f, 0f, 255f,
|
||||||
|
0f, 0f, -1f, 0f, 255f,
|
||||||
|
0f, 0f, 0f, 1f, 0f
|
||||||
|
)
|
||||||
|
),
|
||||||
|
ImageFilter(
|
||||||
|
"Warm", floatArrayOf(
|
||||||
|
1.15f, 0f, 0f, 0f, 10f,
|
||||||
|
0f, 1.05f, 0f, 0f, 5f,
|
||||||
|
0f, 0f, 0.85f, 0f, 0f,
|
||||||
|
0f, 0f, 0f, 1f, 0f
|
||||||
|
)
|
||||||
|
),
|
||||||
|
ImageFilter(
|
||||||
|
"Cool", floatArrayOf(
|
||||||
|
0.85f, 0f, 0f, 0f, 0f,
|
||||||
|
0f, 1f, 0f, 0f, 0f,
|
||||||
|
0f, 0f, 1.15f, 0f, 10f,
|
||||||
|
0f, 0f, 0f, 1f, 0f
|
||||||
|
)
|
||||||
|
),
|
||||||
|
ImageFilter(
|
||||||
|
"Contrast", floatArrayOf(
|
||||||
|
1.5f, 0f, 0f, 0f, -64f,
|
||||||
|
0f, 1.5f, 0f, 0f, -64f,
|
||||||
|
0f, 0f, 1.5f, 0f, -64f,
|
||||||
|
0f, 0f, 0f, 1f, 0f
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
fun decodeSourceBitmap(context: Context): Bitmap {
|
||||||
|
val options = BitmapFactory.Options().apply { inSampleSize = 4 }
|
||||||
|
return BitmapFactory.decodeResource(context.resources, R.drawable.large_image, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createFilteredBitmap(source: Bitmap, filter: ImageFilter): Bitmap {
|
||||||
|
val result = Bitmap.createBitmap(source.width, source.height, Bitmap.Config.ARGB_8888)
|
||||||
|
val canvas = Canvas(result)
|
||||||
|
val paint = Paint(Paint.FILTER_BITMAP_FLAG).apply {
|
||||||
|
colorFilter = ColorMatrixColorFilter(ColorMatrix(filter.matrix))
|
||||||
|
}
|
||||||
|
canvas.drawBitmap(source, 0f, 0f, paint)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
fun shareBitmap(context: Context, bitmap: Bitmap, filterName: String) {
|
||||||
|
val shareDir = File(context.cacheDir, "shared_images").apply { mkdirs() }
|
||||||
|
val file = File(shareDir, "filtered_${filterName.lowercase()}.png")
|
||||||
|
FileOutputStream(file).use { stream ->
|
||||||
|
bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream)
|
||||||
|
}
|
||||||
|
|
||||||
|
val uri = FileProvider.getUriForFile(context, "${context.packageName}.fileprovider", file)
|
||||||
|
val intent = Intent(Intent.ACTION_SEND).apply {
|
||||||
|
type = "image/png"
|
||||||
|
putExtra(Intent.EXTRA_STREAM, uri)
|
||||||
|
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
|
}
|
||||||
|
context.startActivity(Intent.createChooser(intent, "Share filtered image"))
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 117 KiB |
Binary file not shown.
Loading…
Reference in new issue