Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Androapp 5497 mobile UI implement image block #81

Merged
merged 5 commits into from
Oct 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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()
}
}
Loading