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)
Related
I've got some configuration in a json file stored at the asset folder of the app.
I need this configuration during my complete app so I thought a CompositionLocalProvider may be a good choice.
But now I realize I need the context for parsing that json file and that doesn't seem to be possible.
Might there be another way to achieve the goal I'm looking for?
This is my implementation so far:
val LocalAppConfiguration = compositionLocalOf {
Configuration.init(LocalContext.current) // <-- not possible
}
Where my Configuration is like:
object Configuration {
lateinit var branding: Branding
fun init(context: Context) {
val gson = GsonBuilder().create()
branding = gson.fromJson(
InputStreamReader(context.assets.open("branding.json")),
Branding::class.java
)
}
}
I would be very grateful if someone could help me further
compositionLocalOf is not a Composable function. So, LocalContext.current can not be used.
I believe you can achieve the similar goal if you move the initialization of branding outside of the default factory. You'd then do the initialization inside your actual composable where you have access to Context.
Here's a sample code to explain what I'm talking about.
val LocalAppConfiguration = compositionLocalOf {
Configuration
}
#Composable
fun RootApp(
isDarkTheme: Boolean = isSystemInDarkTheme(),
content: #Composable () -> Unit
) {
val brandedConfiguration = Configuration.init(LocalContext.current)
MaterialTheme {
CompositionLocalProvider(LocalAppConfiguration provides brandedConfiguration) {
//your app screen composable here.
}
}
}
Note that you will also have to modify your init method slightly.
object Configuration {
lateinit var branding: Branding
fun init(context: Context) : Configuration {
val gson = GsonBuilder().create()
branding = gson.fromJson(
InputStreamReader(context.assets.open("branding.json")),
Branding::class.java
)
return this
}
}
So the return function of my view model gives no return where in it is expected to give a return of bitmap so it can be used in UI to set the image.
Code of View Model :
val bitmap : MutableLiveData<Bitmap> by lazy { MutableLiveData<Bitmap>() }
fun retrive(doc_name : String,uid: String){
viewModelScope.launch(Dispatchers.IO){
bitmap.postValue(repository.retrive(doc_name,uid))
}
}
Code of Repository:
var localfile = createTempFile("tempImage", null)
var bitmap :Bitmap? = null
override suspend fun retrive(doc_name:String,uid: String) : Bitmap?{
val storageRef = FirebaseStorage.getInstance().reference?.child("/image/8WEFQnomCEMtlaSkCIkrBgT7XeO2/download")
storageRef.getFile(localfile).addOnSuccessListener {
bitmap = BitmapFactory.decodeFile(localfile.absolutePath)
}
return bitmap
}
Code in Fragment inside on View Created part:
val obsover = Observer<Bitmap>{
image.setImageBitmap(it)
}
admin_viewmodel.bitmap.observe(viewLifecycleOwner,obsover)
So because I kept in my Repository function that bitmap can be null it opens the fragment with no image in image view
But if I keep the Bitmap to not be null(!!) the app crashes and gives the Null Pointer Exception error in the lines below:
Inside the repository code I shared above:
return bitmap!!
2.Inside the View Model code I shared above:
bitmap.postValue(repository.retrive(doc_name,uid))
What I think is its Unable to return because things are working on different threads.
Kindy help me solve this, Thanks.
Edit after Broot Reply code changes:
override suspend fun retrive(doc_name:String,uid: String) : Bitmap {
return suspendCoroutine { cont ->
val storageRef =
FirebaseStorage.getInstance().reference?.child("/image/0XhL4jD4XCemk38rcRkIEjJMgjh2/Aadhar")
val localfile = createTempFile("tempImage", null)
storageRef.getFile(localfile).addOnSuccessListener {
val x = cont.resume(bitmap!!)
Log.d("checck", "$x")
}
}
}
Your original code was incorrect because it fires off an asynchronous function to the API and then returns immediately, before that asynchronous work is done and has fired its callback to update the bitmap property.
Your second code is wrong, because it tries to resume the continuation with the value of the property bitmap, which you have not updated with the value that was returned in the callback. Also, since you're just wanting a Bitmap from the cloud file, there's no reason to download it to a temporary file. You can work directly with the bytes. And there's no reason to use a property that I can see. bitmap can be a local variable.
Also, since you don't do anything in case of failure, your function would hang if there is a problem retrieving the data from Firebase. Below, I just throw the error, but you could do something different like returning null if you want.
I don't know what you're doing with those two parameters, but I left them. I leave it up to you to decide what your byte limit should be (I just used 5 million bytes). I don't remember the guaranteed minimum amount of available memory is for an Android app, and you might know that the file you're retrieving is below that value anyway.
override suspend fun retrive(doc_name: String, uid: String): Bitmap = suspendCoroutine { cont ->
val storageRef =
FirebaseStorage.getInstance().reference.child("/image/0XhL4jD4XCemk38rcRkIEjJMgjh2/Aadhar")
storageRef.getBytes(5_000_000L).addOnSuccessListener { byteArray ->
val bitmap = BitmapFactory.decodeByteArray(byteArray)
cont.resume(bitmap)
}.addOnFailureListener {
cont.resumeWithException(it)
}
}
However: Firebase already comes with the await() extension suspend function so you don't have to use suspendCoroutine.
override suspend fun retrive(doc_name: String, uid: String): Bitmap {
val storageRef =
FirebaseStorage.getInstance().reference.child("/image/0XhL4jD4XCemk38rcRkIEjJMgjh2/Aadhar")
val byteArray = storageRef.getBytes(5_000_000L).await()
return BitmapFactory.decodeByteArray(byteArray)
}
Since decoding a bitmap is kind of a heavy operation, I would do this in Dispatchers.Default:
override suspend fun retrive(doc_name: String, uid: String): Bitmap = withContext(Dispatchers.Default) {
val storageRef =
FirebaseStorage.getInstance().reference.child("/image/0XhL4jD4XCemk38rcRkIEjJMgjh2/Aadhar")
val byteArray = storageRef.getBytes(5_000_000L).await()
return BitmapFactory.decodeByteArray(byteArray)
}
I have the below working code which uses a dropdown to update the satusFilterFlow to allow for the filtering of characters through the getCharacterList call. The getCharacterList call uses the jetpack paging and returns Flow<PagerData<Character>>.
private val statusFilterFlow = MutableStateFlow<StatusFilter>(NoStatusFilter)
// private val searchFilterFlow = MutableStateFlow<SearchFilter>(NoSearchFilter)
val listData: LiveData<PagingData<Character>> =
statusFilterFlow.flatMapLatest{ statusFilter ->
characterRepository.getCharacterList(null, statusFilter.status)
.cachedIn(viewModelScope)
.flowOn(Dispatchers.IO)
}.asLiveData()
Given the above working solution, what is the correct flow extension to allow for me to add multiple StateFlows as I build out additional filters (e.g. SearchFilter).
I have tried combineTransorm as follows:
private val statusFilterFlow = MutableStateFlow<StatusFilter>(NoStatusFilter)
private val searchFilterFlow = MutableStateFlow<SearchFilter>(NoSearchFilter)
val listData: LiveData<PagingData<Character>> =
statusFilterFlow.combineTransform(searchFilterFlow) { statusFilter, searchFilter ->
characterRepository.getCharacterList(searchFilter.search, statusFilter.status)
.flowOn(Dispatchers.IO)
.cachedIn(viewModelScope)
}.asLiveData()
However, this gives me a "Not enough information to infer type variable R" error.
The usual way to understand and/or fix those errors is to specify types explicitly in the function call:
statusFilterFlow.combineTransform<StatusFilter, SearchFilter, PagingData<Character>>(searchFilterFlow) { ... }
This is orthogonal to the problem at hand, but I'd also suggest using the top-level combineTransform overload that takes all source flows as argument (instead of having the first one as receiver), so there is a better symmetry. Since I believe there is no reason one of the filters is more special than the other.
All in all, this gives:
val listData: LiveData<PagingData<Character>> =
combineTransform<StatusFilter, SearchFilter, PagingData<Character>>(statusFilterFlow, searchFilterFlow) { statusFilter, searchFilter ->
characterRepository.getCharacterList(searchFilter.search, statusFilter.status)
.flowOn(Dispatchers.IO)
.cachedIn(viewModelScope)
}.asLiveData()
For anymore else, this is too complex or doesn't work out for you ... Use Combine then flatMap latest on the top of that.
private val _selectionLocation: MutableStateFlow<Location?> = MutableStateFlow(null)
val searchKeyword: MutableStateFlow<String> = MutableStateFlow("")
val unassignedJobs: LiveData<List<Job>> =
combine(_selectionLocation, searchKeyword) { location: Location?, keyword: String ->
Log.e("HomeViewModel", "$location -- $keyword")
location to keyword
}.flatMapLatest { pair ->
_repo.getJob(Status.UNASSIGNED, pair.first).map {
Log.e("HomeViewModel", "size ${it.size}")
it.filter { it.desc.contains(pair.second) }
}
}.flowOn(Dispatchers.IO).asLiveData(Dispatchers.Main)
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")
}
}
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
)