I need to disable filtering in Image to display pixel art properly. How can I do it?
I used to do it like in this answer:
val DRAW_FILTER = PaintFlagsDrawFilter(Paint.FILTER_BITMAP_FLAG, 0)
#SuppressLint("RestrictedApi")
class AliasingDrawableWrapper(wrapped: Drawable) : DrawableWrapper(wrapped) {
override fun draw(canvas: Canvas) {
val oldDrawFilter = canvas.drawFilter
canvas.drawFilter = DRAW_FILTER
super.draw(canvas)
canvas.drawFilter = oldDrawFilter
}
}
and
imageView.setImageDrawable(AliasingDrawableWrapper(drawable)
At least fort now, you can't. Jetpack Compose uses a shared Paint created with the anti alias flag on.
I opened an issue asking for the possibility to set our own flags:
https://issuetracker.google.com/issues/172473708
Here's the Compose's declaration in AndroidPaint.kt:
internal fun makeNativePaint() =
android.graphics.Paint(android.graphics.Paint.ANTI_ALIAS_FLAG)
Since Compose 1.1.0-alpha01 Image takes an optional FilterQuality parameter. Use FilterQuality.None for pixel art:
Image(
...,
filterQuality = FilterQuality.None
)
Related
I'm new to Jetpack Compose and trying to figure out how to solve next task:
I need to create a simple transparent AndroidView. And it needs to be used as an overlay for Composable functions.
The problem is that an overlay should be the same size as a compose view under it.
I had some-kind of successful attempt with this:
#Composable
fun BugseeOverlayView() {
AndroidView(
modifier = Modifier.fillMaxSize(),
factory = { ctx ->
View(ctx).apply {
layoutParams = LinearLayout.LayoutParams(200, 200) //Hardcoded size
alpha = 0.0F
}
}, update = {
Bugsee.addSecureView(it) // 3rd party I need to use
}
)
}
And then I used it like:
Box {
Box(Modifier.fillMaxSize()) {
BugseeOverlayView()
}
Text("Hide me") // or some 'CustomComposableView(param)'
}
This works, but the size is hardcoded.
PS. I need an AndroidView because of third-party tool which accepts android.view.View as a parameter.
You can get size of a Composable in various ways.
1- Modifier.onSizeChanged{intSize->} will return Composable size in pixels you can convert this to dp using LocalDensity.current.run{}. With this approach the size you set will change and there needs to be another recomposition. You can also get size of a Composable from Modifier.onGloballyPositioned either.
val density = LocalDensity.current
var dpSize: DpSize by remember{ mutableStateOf(DpSize.Zero) }
Modifier.onSizeChanged { size: IntSize ->
density.run { dpSize = DpSize(size.width.toDp(), size.height.toDp()) }
}
Modifier.onGloballyPositioned {layoutCoordinates: LayoutCoordinates ->
val size = layoutCoordinates.size
density.run { dpSize = DpSize(size.width.toDp(), size.height.toDp()) }
}
2- If the Composable has fixed size or covers screen you can use
BoxWithConstraints {
SomeComposable()
AndroidView(modifier=Modifier.size(maxWidth, maxHeight)
}
3- If you don't have chance to get Composable size and don't want to have another recomposition you can use SubcomposeLayout. Detailed answer is available here how to create a SubcomposeLayout to get exact size of a Composable without recomposition.
When you are able to get size of Composable you can set same size to AndroidView and set layout params to match parent. If that's not what you wish you can still set Modifier.fillMaxSize while using methods above to set layout params
I would like to capture the UI emitted by Jetpack compose as a Bitmap. In XML this was done like this:
Basically takes a view as an input parameter and returns it as a Bitmap.
//take screenshot of the view added as an input argument
fun takeScreenShot(view: View) : Bitmap {
val bitmap = Bitmap.createBitmap(
view.width,
view.height,
Bitmap.Config.ARGB_8888
)
val canvas = Canvas(bitmap)
view.draw(canvas)
return bitmap
}
What is the equivalent of this in Jetpack compose?
Taking screenshots from a composable is possible in tests.
For taking screenshots in production code see this question and this issue.
First, make sure you have the following dependency in your build script (along with other required Compose dependencies):
debugImplementation("androidx.compose.ui:ui-test-manifest:<version>")
Note: Instead of the above dependency, you can simply add an AndroidManifest.xml in androidTest directory and add the following in manifest>application element: <activity android:name="androidx.activity.ComponentActivity" />.
Refer to this answer.
Here is a complete example for saving, reading, and comparing screenshots:
(Please refer to this post for setting up write permissions and so on for the tests)
class ScreenshotTest {
#get:Rule val composeTestRule = createComposeRule()
#Test fun takeAndSaveScreenshot() {
composeTestRule.setContent { MyComposableFunction() }
val node = composeTestRule.onRoot()
val screenshot = node.captureToImage().asAndroidBitmap()
saveScreenshot("screenshot.png", screenshot)
}
#Test fun readAndCompareScreenshots() {
composeTestRule.setContent { MyComposableFunction() }
val node = composeTestRule.onRoot()
val screenshot = node.captureToImage().asAndroidBitmap()
val context = InstrumentationRegistry.getInstrumentation().targetContext
val path = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES)
val file = File(path, "screenshot.png")
val saved = readScreenshot(file)
println("Are screenshots the same: ${screenshot.sameAs(saved)}")
}
private fun readScreenshot(file: File) = BitmapFactory.decodeFile(file.path)
private fun saveScreenshot(filename: String, screenshot: Bitmap) {
val context = InstrumentationRegistry.getInstrumentation().targetContext
// Saves in /Android/data/your.package.name.test/files/Pictures on external storage
val path = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES)
val file = File(path, filename)
file.outputStream().use { stream ->
screenshot.compress(Bitmap.CompressFormat.PNG, 100, stream)
}
}
}
Thanks to Google Codelabs for this.
I would look at how JP-Compose testing does this.
A good starting point could be the android-compose-codelab, see:
/**
* Simple on-device screenshot comparator that uses golden images present in
* `androidTest/assets`. It's used to showcase the [AnimationClockTestRule] used in
* [AnimatingCircleTests].
*
* Minimum SDK is O. Densities between devices must match.
*
* Screenshots are saved on device in `/data/data/{package}/files`.
*/
#RequiresApi(Build.VERSION_CODES.O)
fun assertScreenshotMatchesGolden(
goldenName: String,
node: SemanticsNodeInteraction
) {
val bitmap = node.captureToImage().asAndroidBitmap()
}
from ScreenshotComparator.kt. You can find captureToImage() here in the AndroidHelpers.kt.
Also you can find here the ImageBitmap.kt, where asAndroidBitmap() only makes sure that, the underlying "common" version of the ImageBitmap is actually an android.graphics.Bitmap on Android (this is to make the code more platform agnostic, so that it can be run on the JVM/desktop as well)
I'm trying to port a rather complex Android View to Compose and I've managed to do a naive implementation by basically using a Canvas and moving the onDraw() code there. I've ran into issues when trying to optimize this to make it skip unneeded parts of the recomposition.
The view is a board for the game of GO (it would be the same for chess). I'm trying to get things such as the board's background to not redraw every time a move is made, as the background does not change. As my understanding of the docs is, if I pull the drawBackground() from the onDraw and just put it in an Image() composable, the Image() composable should not get recomposed unless its parameter (which is just the bitmap) changes. However, breakpoints show the method getting called every single time the position changes (e.g. the player makes a move). Am I doing something wrong? How could I take advantage of Compose here?
Code:
#Composable
fun Board(modifier: Modifier = Modifier, boardSize: Int, position: Position?, candidateMove: Point?, candidateMoveType: StoneType?, onTapMove: ((Point) -> Unit)? = null, onTapUp: ((Point) -> Unit)? = null) {
val background: ImageBitmap = imageResource(id = R.mipmap.texture)
Box(modifier = modifier
.fillMaxWidth()
.aspectRatio(1f)
) {
Image(bitmap = background) // Expecting this to run only once, but gets run every time Board() gets recomposed!!!
var width by remember { mutableStateOf(0) }
val measurements = remember(width, boardSize) { doMeasurements(width, boardSize, drawCoordinates) }
var lastHotTrackedPoint: Point? by remember { mutableStateOf(null) }
Canvas(modifier = Modifier.fillMaxSize()) {
if (measurements.width == 0) {
return#Canvas
}
//... lots of draw code here
}
}
}
Any Jetpack Compose guru can help me understand why is it not skipping that recomposition?
Try putting Image(bitmap = background) in a separate composable function. Or moving Canvas to another function.
It looks like the only way to go about loading custom icons from Android Vector Resources in the res folder is to do it within a #Composable function using the vectorResource(R.drawable.myVectorName) method.
This is great and all, but I like the syntax of fetching VectorAssets for the Icon(asset: VectorAsset) class, which looks like Icon(Icons.Default.Plus).
It looks like the vectorResource() method uses an internal method called loadVectorResource(), and the methods it uses to read the actual XML file composing the vector asset file are also internal.
How would I go about creating an object like MyAppIcons.Default.SomeIcon in Jetpack Compose?
EDIT
So, I have sort-of found a solution. However, it would be nice to make my own extension/overloading of the built-in Icon() function, but I'm not sure if there is a proper way to do this.
from Resources in Compose
Use the painterResource API to load either vector drawables or rasterized asset formats like PNGs. You don't need to know the type of the drawable, simply use painterResource in Image composables or paint modifiers.
// Files in res/drawable folders. For example:
// - res/drawable-nodpi/ic_logo.xml
// - res/drawable-xxhdpi/ic_logo.png
// In your Compose code
Icon(
painter = painterResource(id = R.drawable.ic_logo),
contentDescription = null // decorative element
)
Turns out I wasn't using my brain. The answer is pretty easy.
The gist is, Icon() is a composable function, meaning that of course vectorResource() can be used there.
So, the correct approach is no secret... it's to make your own MyAppIcon() component, call vectorResource() and then return a normal Icon(), like so:
Correct Way
#Composable
fun MyAppIcon(
resourceId: Int,
modifier: Modifier = Modifier,
tint: Color = AmbientContentColor.current
) {
Icon(
asset = vectorResource(id = resourceId),
modifier = modifier,
tint = tint
)
}
You can then create an object elsewhere, like so:
object MyAppIcons {
val SomeIcon = R.drawable.someIcon
val AnotherIcon = R.drawable.anotherIcon
}
When you put the two together, you can use it like this:
MyAppIcon(MyAppIcons.SomeIcon)
I'm hoping that Google just adds this override soon, allowing us to pass in resource IDs.
There is a way to load asset using Icon(Icons.Default.Plus). You need to make an extesion property
val androidx.compose.material.icons.Icons.Filled.FiveG : VectorAsset
get() {
}
but I don't see the way to get VectorAsset outside of composable function.
Of course you can do something like this
val androidx.compose.material.icons.Icons.Filled.FiveG : VectorAsset
get() {
return Assets.FiveG
}
object Assets {
lateinit var FiveG: VectorAsset
}
#Composable
fun initializeAssets() {
Assets.FiveG = vectorResource(R.drawable.ic_baseline_5g_24)
}
but it's a bad idea to have a composable with side effect. So i'm waiting for someone to find a way to convert SVG to VectorAsset Kotlin class or get VectorAsset object outside of composable function.
I went down the other route and extracted the logic from Jetpack Compose source code that turns an XML SVG path string into an ImageVector. In the end I came up with this:
fun makeIconFromXMLPath(
pathStr: String,
viewportWidth: Float = 24f,
viewportHeight: Float = 24f,
defaultWidth: Dp = 24.dp,
defaultHeight: Dp = 24.dp,
fillColor: Color = Color.White,
): ImageVector {
val fillBrush = SolidColor(fillColor)
val strokeBrush = SolidColor(fillColor)
return ImageVector.Builder(
defaultWidth = defaultWidth,
defaultHeight = defaultHeight,
viewportWidth = viewportWidth,
viewportHeight = viewportHeight,
).run {
addPath(
pathData = addPathNodes(pathStr),
name = "",
fill = fillBrush,
stroke = strokeBrush,
)
build()
}
}
All you have to do is call this function with pathStr set to the value of android:pathData from the drawable XML file. Here's an example:
val AppleIcon by lazy { makeAppleIcon() }
// by Austin Andrews, found on https://materialdesignicons.com/
private fun makeAppleIcon(): ImageVector {
return makeIconFromXMLPath(
pathStr = "M20,10C22,13 17,22 15,22C13,22 13,21 12,21C11,21 11,22 9,22C7,22 2,13 4,10C6,7 9,7 11,8V5C5.38,8.07 4.11,3.78 4.11,3.78C4.11,3.78 6.77,0.19 11,5V3H13V8C15,7 18,7 20,10Z"
)
}
#Preview
#Composable
fun AppleIconPreview() {
Surface {
Icon(AppleIcon, "Apple")
}
}
So, I have implemented a lazycolumnfor to work with a list of recipe elements, the thing is that it does not smooth scroll, if I just scroll fast it stutters till the last element appears and not smooth scroll.
Is this an error from my side or do I need to add something else?
data class Recipe(
#DrawableRes val imageResource: Int,
val title: String,
val ingredients: List<String>
)
val recipeList = listOf(
Recipe(R.drawable.header,"Cake1", listOf("Cheese","Sugar","water")),
Recipe(R.drawable.header,"Cake2", listOf("Cheese1","Sugar1","Vanilla")),
Recipe(R.drawable.header,"Cake3", listOf("Bread","Sugar2","Apple")))
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
RecipeList(recipeList = recipeList)
}
}
}
#Composable
fun RecipeCard(recipe:Recipe){
val image = imageResource(R.drawable.header)
Surface(shape = RoundedCornerShape(8.dp),elevation = 8.dp,modifier = Modifier.padding(8.dp)) {
Column(modifier = Modifier.padding(16.dp)) {
val imageModifier = Modifier.preferredHeight(150.dp).fillMaxWidth().clip(shape = RoundedCornerShape(8.dp))
Image(asset = image,modifier = imageModifier,contentScale = ContentScale.Crop)
Spacer(modifier = Modifier.preferredHeight(16.dp))
Text(text = recipe.title,style = typography.h6)
for(ingredient in recipe.ingredients){
Text(text = ingredient,style = typography.body2)
}
}
}
}
#Composable
fun RecipeList(recipeList:List<Recipe>){
LazyColumnFor(items = recipeList) { item ->
RecipeCard(recipe = item)
}
}
#Preview
#Composable
fun RecipePreview(){
RecipeCard(recipeList[0])
}
Currently (version 1.0.0-alpha02) Jetpack Compose has 2 Composable functions for loading image resources:
imageResource(): this Composable function, load an image resource synchronously.
loadImageResource(): this function loads the image in a background thread, and once the loading finishes, recompose is scheduled and this function will return deferred image resource with LoadedResource or FailedResource
So your lazyColumn is not scrolling smoothly since you are loading images synchronously.
So you should either use loadImageResource() or a library named Accompanist by Chris Banes, which can fetch and display images from external sources, such as network, using the Coil image loading library.
UPDATE:
Using CoilImage :
First, add Accompanist Gradle dependency, then simply use CoilImage composable function:
CoilImage(data = R.drawable.header)
Using loadImageResource() :
val deferredImage = loadImageResource(
id = R.drawable.header,
)
val imageModifier = Modifier.preferredHeight(150.dp).fillMaxWidth()
.clip(shape = RoundedCornerShape(8.dp))
deferredImage.resource.resource?.let {
Image(
asset = it,
modifier = imageModifier
)
}
Note: I tried both ways in a LazyColumnFor, and although loadImageResource() performed better than imageResource() but still it didn't scroll smoothly.
So I highly recommend using CoilImage
Note 2: To use Glide or Picasso, check this repository by Vinay Gaba
On the other note, LazyColumn haven't been optimised for scrolling performance yet, but I've just tested on 1.0.0-beta07 release and can confirm it's way smoother than 1.0.0-beta06
Compose.UI 1.0.0-beta07 relevant change log:
LazyColumn/Row will now keep up to 2 previously visible items active (not disposed) even when they are scrolled out already. This allows the component to reuse the active subcompositions when we will need to compose a new item which improves the scrolling performance. (Ie5555)