Skip to content

Commit

Permalink
Androapp 5497 mobile UI implement image block (#81)
Browse files Browse the repository at this point in the history
* add component

* add image block component

* update component to make more generic

* fix lint error

* fix image block scale type to crop

---------

Co-authored-by: Siddharth Agarwal <[email protected]>
  • Loading branch information
siddh1004 and Siddharth Agarwal authored Oct 10, 2023
1 parent dd42d28 commit f61e587
Show file tree
Hide file tree
Showing 10 changed files with 180 additions and 1 deletion.
5 changes: 4 additions & 1 deletion common/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ kotlin {
implementation(compose.foundation)
implementation(compose.ui)
implementation(compose.material3)
@OptIn(org.jetbrains.compose.ExperimentalComposeLibrary::class)
implementation(compose.components.resources)
implementation(project(":designsystem"))
}
}
Expand Down Expand Up @@ -49,7 +51,8 @@ android {
namespace = "org.hisp.dhis.mobile.ui.common"

sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml")
sourceSets["main"].res.srcDirs("src/androidMain/res")
sourceSets["main"].res.srcDirs("src/androidMain/res", "src/commonMain/resources")
sourceSets["main"].resources.srcDirs("src/commonMain/resources")

defaultConfig {
minSdk = (findProperty("android.minSdk") as String).toInt()
Expand Down
2 changes: 2 additions & 0 deletions common/src/commonMain/kotlin/org/hisp/dhis/common/App.kt
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import org.hisp.dhis.common.screens.Components
import org.hisp.dhis.common.screens.FormShellsScreen
import org.hisp.dhis.common.screens.FormsComponentsScreen
import org.hisp.dhis.common.screens.IconButtonScreen
import org.hisp.dhis.common.screens.ImageBlockScreen
import org.hisp.dhis.common.screens.InputAgeScreen
import org.hisp.dhis.common.screens.InputCheckBoxScreen
import org.hisp.dhis.common.screens.InputEmailScreen
Expand Down Expand Up @@ -171,6 +172,7 @@ fun Main() {
Components.INPUT_EMAIL -> InputEmailScreen()
Components.CAROUSEL_BUTTONS -> ButtonCarouselScreen()
Components.INPUT_ORG_UNIT -> InputOrgUnitScreen()
Components.IMAGE_BLOCK -> ImageBlockScreen()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,5 @@ enum class Components(val label: String) {
INPUT_EMAIL("Input Email"),
CAROUSEL_BUTTONS("Carousel buttons"),
INPUT_ORG_UNIT("Input Org. Unit"),
IMAGE_BLOCK("Image Block"),
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package org.hisp.dhis.common.screens

import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.graphics.painter.Painter
import org.hisp.dhis.mobile.ui.designsystem.component.ImageBlock
import org.jetbrains.compose.resources.ExperimentalResourceApi
import org.jetbrains.compose.resources.painterResource

@Composable
fun ImageBlockScreen() {
val sampleImage = provideSampleImage()
ImageBlock(
load = { sampleImage },
painterFor = { remember { it } },
onClick = {},
)
}

@OptIn(ExperimentalResourceApi::class)
@Composable
private fun provideSampleImage(): Painter =
painterResource("drawable/sample.png")
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package org.hisp.dhis.mobile.ui.designsystem.component.internal.image

import android.graphics.BitmapFactory
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.asImageBitmap
import java.io.File

actual fun provideImage(file: File): ImageBitmap? {
return try {
BitmapFactory.decodeFile(file.absolutePath).asImageBitmap()
} catch (ex: Exception) {
null
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package org.hisp.dhis.mobile.ui.designsystem.component

import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.FileDownload
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.produceState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.hisp.dhis.mobile.ui.designsystem.theme.Radius
import org.hisp.dhis.mobile.ui.designsystem.theme.Spacing
import java.io.IOException

/**
* DHIS2 Image Block. Wraps compose [Image].
* @param load to load an image stored in the resource, device memory or from network
* we can use loadPainter, loadImageBitmap, loadSvgPainter or loadXmlImageVector
* @param painterFor is a composable function which controls how to paint the load param,
* @param modifier allows a modifier to be passed externally
* @param downloadButtonVisible controls whether the download button is visible or not
* @param onClick is a callback to notify when the download button is clicked.
*/
@Composable
fun <T> ImageBlock(
load: suspend () -> T,
painterFor: @Composable (T) -> Painter,
modifier: Modifier = Modifier,
downloadButtonVisible: Boolean = true,
onClick: () -> Unit,
) {
val image: T? by produceState<T?>(null) {
value = withContext(Dispatchers.IO) {
try {
load()
} catch (e: IOException) {
null
}
}
}

if (image != null) {
Box(
modifier = modifier
.padding(vertical = Spacing.Spacing8)
.testTag("IMAGE_BLOCK_CONTAINER"),
) {
Image(
painter = painterFor(image!!),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier
.fillMaxWidth()
.clip(shape = RoundedCornerShape(Radius.S))
.height(160.dp),
)
if (downloadButtonVisible) {
SquareIconButton(
enabled = true,
modifier = Modifier
.align(Alignment.BottomEnd)
.padding(Spacing.Spacing4),
icon = {
Icon(
imageVector = Icons.Outlined.FileDownload,
contentDescription = "File download Button",
)
},
) {
onClick.invoke()
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package org.hisp.dhis.mobile.ui.designsystem.component.internal.image

import androidx.compose.ui.graphics.ImageBitmap
import java.io.File

expect fun provideImage(file: File): ImageBitmap?
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package org.hisp.dhis.mobile.ui.designsystem.component.internal.image

import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.toComposeImageBitmap
import org.jetbrains.skia.Image
import java.io.File

actual fun provideImage(file: File): ImageBitmap? {
return try {
Image.makeFromEncoded(file.readBytes()).toComposeImageBitmap()
} catch (ex: Exception) {
null
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package org.hisp.dhis.mobile.ui.designsystem.component

import androidx.compose.ui.graphics.painter.BitmapPainter
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithTag
import org.hisp.dhis.mobile.ui.designsystem.component.internal.image.provideImage
import org.junit.Rule
import org.junit.Test
import java.io.File

class ImageBlockTest {

@get:Rule
val rule = createComposeRule()

@Test
fun shouldNotRenderImageBlockIfFileIsNotValid() {
rule.setContent {
ImageBlock(
load = { provideImage(File("")) },
painterFor = { BitmapPainter(it!!) },
onClick = {},
)
}

rule.onNodeWithTag("IMAGE_BLOCK_CONTAINER").assertDoesNotExist()
}
}

0 comments on commit f61e587

Please sign in to comment.