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